]> arthur.barton.de Git - netdata.git/blob - python.d/python_modules/base.py
0482b9b8059de1c344142118e4f24e861184c091
[netdata.git] / python.d / python_modules / base.py
1 # -*- coding: utf-8 -*-
2 # Description: netdata python modules framework
3 # Author: Pawel Krupa (paulfantom)
4
5 # Remember:
6 # ALL CODE NEEDS TO BE COMPATIBLE WITH Python > 2.7 and Python > 3.1
7 # Follow PEP8 as much as it is possible
8 # "check" and "create" CANNOT be blocking.
9 # "update" CAN be blocking
10 # "update" function needs to be fast, so follow:
11 #   https://wiki.python.org/moin/PythonSpeed/PerformanceTips
12 # basically:
13 #  - use local variables wherever it is possible
14 #  - avoid dots in expressions that are executed many times
15 #  - use "join()" instead of "+"
16 #  - use "import" only at the beginning
17 #
18 # using ".encode()" in one thread can block other threads as well (only in python2)
19
20 import time
21 import os
22 import socket
23 import select
24 import threading
25 import msg
26 import ssl
27 from subprocess import Popen, PIPE
28 from sys import exc_info
29
30 try:
31     import urllib.request as urllib2
32 except ImportError:
33     import urllib2
34
35 try:
36     import MySQLdb
37     PYMYSQL = True
38 except ImportError:
39     try:
40         import pymysql as MySQLdb
41         PYMYSQL = True
42     except ImportError:
43         PYMYSQL = False
44
45 try:
46     PATH = os.getenv('PATH').split(':')
47 except AttributeError:
48     PATH = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'.split(':')
49
50
51 # class BaseService(threading.Thread):
52 class SimpleService(threading.Thread):
53     """
54     Prototype of Service class.
55     Implemented basic functionality to run jobs by `python.d.plugin`
56     """
57     def __init__(self, configuration=None, name=None):
58         """
59         This needs to be initialized in child classes
60         :param configuration: dict
61         :param name: str
62         """
63         threading.Thread.__init__(self)
64         self._data_stream = ""
65         self.daemon = True
66         self.retries = 0
67         self.retries_left = 0
68         self.priority = 140000
69         self.update_every = 1
70         self.name = name
71         self.override_name = None
72         self.chart_name = ""
73         self._dimensions = []
74         self._charts = []
75         self.__chart_set = False
76         self.__first_run = True
77         self.order = []
78         self.definitions = {}
79         self._data_from_check = dict()
80         if configuration is None:
81             self.error("BaseService: no configuration parameters supplied. Cannot create Service.")
82             raise RuntimeError
83         else:
84             self._extract_base_config(configuration)
85             self.timetable = {}
86             self.create_timetable()
87
88     # --- BASIC SERVICE CONFIGURATION ---
89
90     def _extract_base_config(self, config):
91         """
92         Get basic parameters to run service
93         Minimum config:
94             config = {'update_every':1,
95                       'priority':100000,
96                       'retries':0}
97         :param config: dict
98         """
99         pop = config.pop
100         try:
101             self.override_name = pop('name')
102         except KeyError:
103             pass
104         self.update_every = int(pop('update_every'))
105         self.priority = int(pop('priority'))
106         self.retries = int(pop('retries'))
107         self.retries_left = self.retries
108         self.configuration = config
109
110     def create_timetable(self, freq=None):
111         """
112         Create service timetable.
113         `freq` is optional
114         Example:
115             timetable = {'last': 1466370091.3767564,
116                          'next': 1466370092,
117                          'freq': 1}
118         :param freq: int
119         """
120         if freq is None:
121             freq = self.update_every
122         now = time.time()
123         self.timetable = {'last': now,
124                           'next': now - (now % freq) + freq,
125                           'freq': freq}
126
127     # --- THREAD CONFIGURATION ---
128
129     def _run_once(self):
130         """
131         Executes self.update(interval) and draws run time chart.
132         Return value presents exit status of update()
133         :return: boolean
134         """
135         t_start = float(time.time())
136         chart_name = self.chart_name
137
138         since_last = int((t_start - self.timetable['last']) * 1000000)
139         if self.__first_run:
140             since_last = 0
141
142         if not self.update(since_last):
143             self.error("update function failed.")
144             return False
145
146         # draw performance graph
147         run_time = int((time.time() - t_start) * 1000)
148         print("BEGIN netdata.plugin_pythond_%s %s\nSET run_time = %s\nEND\n" %
149               (self.chart_name, str(since_last), str(run_time)))
150
151         self.debug(chart_name, "updated in", str(run_time), "ms")
152         self.timetable['last'] = t_start
153         self.__first_run = False
154         return True
155
156     def run(self):
157         """
158         Runs job in thread. Handles retries.
159         Exits when job failed or timed out.
160         :return: None
161         """
162         step = float(self.timetable['freq'])
163         penalty = 0
164         self.timetable['last'] = float(time.time() - step)
165         self.debug("starting data collection - update frequency:", str(step), " retries allowed:", str(self.retries))
166         while True:  # run forever, unless something is wrong
167             now = float(time.time())
168             next = self.timetable['next'] = now - (now % step) + step + penalty
169
170             # it is important to do this in a loop
171             # sleep() is interruptable
172             while now < next:
173                 self.debug("sleeping for", str(next - now), "secs to reach frequency of", str(step), "secs, now:", str(now), " next:", str(next), " penalty:", str(penalty))
174                 time.sleep(next - now)
175                 now = float(time.time())
176
177             # do the job
178             try:
179                 status = self._run_once()
180             except Exception as e:
181                 status = False
182
183             if status:
184                 # it is good
185                 self.retries_left = self.retries
186                 penalty = 0
187             else:
188                 # it failed
189                 self.retries_left -= 1
190                 if self.retries_left <= 0:
191                     if penalty == 0:
192                         penalty = float(self.retries * step) / 2
193                     else:
194                         penalty *= 1.5
195
196                     if penalty > 600:
197                         penalty = 600
198
199                     self.retries_left = self.retries
200                     self.alert("failed to collect data for " + str(self.retries) + " times - increasing penalty to " + str(penalty) + " sec and trying again")
201
202                 else:
203                     self.error("failed to collect data - " + str(self.retries_left) + " retries left - penalty: " + str(penalty) + " sec")
204
205     # --- CHART ---
206
207     @staticmethod
208     def _format(*args):
209         """
210         Escape and convert passed arguments.
211         :param args: anything
212         :return: list
213         """
214         params = []
215         append = params.append
216         for p in args:
217             if p is None:
218                 append(p)
219                 continue
220             if type(p) is not str:
221                 p = str(p)
222             if ' ' in p:
223                 p = "'" + p + "'"
224             append(p)
225         return params
226
227     def _line(self, instruction, *params):
228         """
229         Converts *params to string and joins them with one space between every one.
230         Result is appended to self._data_stream
231         :param params: str/int/float
232         """
233         tmp = list(map((lambda x: "''" if x is None or len(x) == 0 else x), params))
234         self._data_stream += "%s %s\n" % (instruction, str(" ".join(tmp)))
235
236     def chart(self, type_id, name="", title="", units="", family="",
237               category="", chart_type="line", priority="", update_every=""):
238         """
239         Defines a new chart.
240         :param type_id: str
241         :param name: str
242         :param title: str
243         :param units: str
244         :param family: str
245         :param category: str
246         :param chart_type: str
247         :param priority: int/str
248         :param update_every: int/str
249         """
250         self._charts.append(type_id)
251
252         p = self._format(type_id, name, title, units, family, category, chart_type, priority, update_every)
253         self._line("CHART", *p)
254
255     def dimension(self, id, name=None, algorithm="absolute", multiplier=1, divisor=1, hidden=False):
256         """
257         Defines a new dimension for the chart
258         :param id: str
259         :param name: str
260         :param algorithm: str
261         :param multiplier: int/str
262         :param divisor: int/str
263         :param hidden: boolean
264         :return:
265         """
266         try:
267             int(multiplier)
268         except TypeError:
269             self.error("malformed dimension: multiplier is not a number:", multiplier)
270             multiplier = 1
271         try:
272             int(divisor)
273         except TypeError:
274             self.error("malformed dimension: divisor is not a number:", divisor)
275             divisor = 1
276         if name is None:
277             name = id
278         if algorithm not in ("absolute", "incremental", "percentage-of-absolute-row", "percentage-of-incremental-row"):
279             algorithm = "absolute"
280
281         self._dimensions.append(str(id))
282         if hidden:
283             p = self._format(id, name, algorithm, multiplier, divisor, "hidden")
284         else:
285             p = self._format(id, name, algorithm, multiplier, divisor)
286
287         self._line("DIMENSION", *p)
288
289     def begin(self, type_id, microseconds=0):
290         """
291         Begin data set
292         :param type_id: str
293         :param microseconds: int
294         :return: boolean
295         """
296         if type_id not in self._charts:
297             self.error("wrong chart type_id:", type_id)
298             return False
299         try:
300             int(microseconds)
301         except TypeError:
302             self.error("malformed begin statement: microseconds are not a number:", microseconds)
303             microseconds = ""
304
305         self._line("BEGIN", type_id, str(microseconds))
306         return True
307
308     def set(self, id, value):
309         """
310         Set value to dimension
311         :param id: str
312         :param value: int/float
313         :return: boolean
314         """
315         if id not in self._dimensions:
316             self.error("wrong dimension id:", id, "Available dimensions are:", *self._dimensions)
317             return False
318         try:
319             value = str(int(value))
320         except TypeError:
321             self.error("cannot set non-numeric value:", str(value))
322             return False
323         self._line("SET", id, "=", str(value))
324         self.__chart_set = True
325         return True
326
327     def end(self):
328         if self.__chart_set:
329             self._line("END")
330             self.__chart_set = False
331         else:
332             pos = self._data_stream.rfind("BEGIN")
333             self._data_stream = self._data_stream[:pos]
334
335     def commit(self):
336         """
337         Upload new data to netdata.
338         """
339         try:
340             print(self._data_stream)
341         except Exception as e:
342             msg.fatal('cannot send data to netdata:', str(e))
343         self._data_stream = ""
344
345     # --- ERROR HANDLING ---
346
347     def error(self, *params):
348         """
349         Show error message on stderr
350         """
351         msg.error(self.chart_name, *params)
352
353     def alert(self, *params):
354         """
355         Show error message on stderr
356         """
357         msg.alert(self.chart_name, *params)
358
359     def debug(self, *params):
360         """
361         Show debug message on stderr
362         """
363         msg.debug(self.chart_name, *params)
364
365     def info(self, *params):
366         """
367         Show information message on stderr
368         """
369         msg.info(self.chart_name, *params)
370
371     # --- MAIN METHODS ---
372
373     def _get_data(self):
374         """
375         Get some data
376         :return: dict
377         """
378         return {}
379
380     def check(self):
381         """
382         check() prototype
383         :return: boolean
384         """
385         self.debug("Module", str(self.__module__), "doesn't implement check() function. Using default.")
386         data = self._get_data()
387
388         if data is None:
389             self.debug("failed to receive data during check().")
390             return False
391
392         if len(data) == 0:
393             self.debug("empty data during check().")
394             return False
395
396         self.debug("successfully received data during check(): '" + str(data) + "'")
397         return True
398
399     def create(self):
400         """
401         Create charts
402         :return: boolean
403         """
404         data = self._data_from_check or self._get_data()
405         if data is None:
406             self.debug("failed to receive data during create().")
407             return False
408
409         idx = 0
410         for name in self.order:
411             options = self.definitions[name]['options'] + [self.priority + idx, self.update_every]
412             self.chart(self.chart_name + "." + name, *options)
413             # check if server has this datapoint
414             for line in self.definitions[name]['lines']:
415                 if line[0] in data:
416                     self.dimension(*line)
417             idx += 1
418
419         self.commit()
420         return True
421
422     def update(self, interval):
423         """
424         Update charts
425         :param interval: int
426         :return: boolean
427         """
428         data = self._get_data()
429         if data is None:
430             self.debug("failed to receive data during update().")
431             return False
432
433         updated = False
434         for chart in self.order:
435             if self.begin(self.chart_name + "." + chart, interval):
436                 updated = True
437                 for dim in self.definitions[chart]['lines']:
438                     try:
439                         self.set(dim[0], data[dim[0]])
440                     except KeyError:
441                         pass
442                 self.end()
443
444         self.commit()
445         if not updated:
446             self.error("no charts to update")
447
448         return updated
449
450     @staticmethod
451     def find_binary(binary):
452         try:
453             if isinstance(binary, str):
454                 binary = os.path.basename(binary)
455                 return next(('/'.join([p, binary]) for p in PATH
456                             if os.path.isfile('/'.join([p, binary]))
457                             and os.access('/'.join([p, binary]), os.X_OK)))
458             else:
459                 return None
460         except StopIteration:
461             return None
462
463
464 class UrlService(SimpleService):
465     # TODO add support for https connections
466     def __init__(self, configuration=None, name=None):
467         self.url = ""
468         self.user = None
469         self.password = None
470         self.proxies = {}
471         SimpleService.__init__(self, configuration=configuration, name=name)
472
473     def __add_openers(self):
474         # TODO add error handling
475         if self.ss_cert:
476             try:
477                 ctx = ssl.create_default_context()
478                 ctx.check_hostname = False
479                 ctx.verify_mode = ssl.CERT_NONE
480                 self.opener = urllib2.build_opener(urllib2.HTTPSHandler(context=ctx))
481             except Exception as error:
482                 self.error(str(error))
483                 self.opener = urllib2.build_opener()
484         else:
485             self.opener = urllib2.build_opener()
486
487         # Proxy handling
488         # TODO currently self.proxies isn't parsed from configuration file
489         # if len(self.proxies) > 0:
490         #     for proxy in self.proxies:
491         #         url = proxy['url']
492         #         # TODO test this:
493         #         if "user" in proxy and "pass" in proxy:
494         #             if url.lower().startswith('https://'):
495         #                 url = 'https://' + proxy['user'] + ':' + proxy['pass'] + '@' + url[8:]
496         #             else:
497         #                 url = 'http://' + proxy['user'] + ':' + proxy['pass'] + '@' + url[7:]
498         #         # FIXME move proxy auth to sth like this:
499         #         #     passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
500         #         #     passman.add_password(None, url, proxy['user'], proxy['password'])
501         #         #     opener.add_handler(urllib2.HTTPBasicAuthHandler(passman))
502         #
503         #         if url.lower().startswith('https://'):
504         #             opener.add_handler(urllib2.ProxyHandler({'https': url}))
505         #         else:
506         #             opener.add_handler(urllib2.ProxyHandler({'https': url}))
507
508         # HTTP Basic Auth
509         if self.user is not None and self.password is not None:
510             passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
511             passman.add_password(None, self.url, self.user, self.password)
512             self.opener.add_handler(urllib2.HTTPBasicAuthHandler(passman))
513             self.debug("Enabling HTTP basic auth")
514
515         #urllib2.install_opener(opener)
516
517     def _get_raw_data(self):
518         """
519         Get raw data from http request
520         :return: str
521         """
522         raw = None
523         try:
524             f = self.opener.open(self.url, timeout=self.update_every * 2)
525             # f = urllib2.urlopen(self.url, timeout=self.update_every * 2)
526         except Exception as e:
527             self.error(str(e))
528             return None
529
530         try:
531             raw = f.read().decode('utf-8', 'ignore')
532         except Exception as e:
533             self.error(str(e))
534         finally:
535             f.close()
536         return raw
537
538     def check(self):
539         """
540         Format configuration data and try to connect to server
541         :return: boolean
542         """
543         if self.name is None or self.name == str(None):
544             self.name = 'local'
545             self.chart_name += "_" + self.name
546         else:
547             self.name = str(self.name)
548         try:
549             self.url = str(self.configuration['url'])
550         except (KeyError, TypeError):
551             pass
552         try:
553             self.user = str(self.configuration['user'])
554         except (KeyError, TypeError):
555             pass
556         try:
557             self.password = str(self.configuration['pass'])
558         except (KeyError, TypeError):
559             pass
560         self.ss_cert = self.configuration.get('ss_cert')
561         self.__add_openers()
562
563         test = self._get_data()
564         if test is None or len(test) == 0:
565             return False
566         else:
567             return True
568
569
570 class SocketService(SimpleService):
571     def __init__(self, configuration=None, name=None):
572         self._sock = None
573         self._keep_alive = False
574         self.host = "localhost"
575         self.port = None
576         self.unix_socket = None
577         self.request = ""
578         self.__socket_config = None
579         self.__empty_request = "".encode()
580         SimpleService.__init__(self, configuration=configuration, name=name)
581
582     def _socketerror(self, message=None):
583         if self.unix_socket is not None:
584             self.error("unix socket '" + self.unix_socket + "':", message)
585         else:
586             if self.__socket_config is not None:
587                 af, socktype, proto, canonname, sa = self.__socket_config
588                 self.error("socket to '" + str(sa[0]) + "' port " + str(sa[1]) + ":", message)
589             else:
590                 self.error("unknown socket:", message)
591
592     def _connect2socket(self, res=None):
593         """
594         Connect to a socket, passing the result of getaddrinfo()
595         :return: boolean
596         """
597         if res is None:
598             res = self.__socket_config
599             if res is None:
600                 self.error("Cannot create socket to 'None':")
601                 return False
602
603         af, socktype, proto, canonname, sa = res
604         try:
605             self.debug("creating socket to '" + str(sa[0]) + "', port " + str(sa[1]))
606             self._sock = socket.socket(af, socktype, proto)
607         except socket.error as e:
608             self.error("Failed to create socket to '" + str(sa[0]) + "', port " + str(sa[1]) + ":", str(e))
609             self._sock = None
610             self.__socket_config = None
611             return False
612
613         try:
614             self.debug("connecting socket to '" + str(sa[0]) + "', port " + str(sa[1]))
615             self._sock.connect(sa)
616         except socket.error as e:
617             self.error("Failed to connect to '" + str(sa[0]) + "', port " + str(sa[1]) + ":", str(e))
618             self._disconnect()
619             self.__socket_config = None
620             return False
621
622         self.debug("connected to '" + str(sa[0]) + "', port " + str(sa[1]))
623         self.__socket_config = res
624         return True
625
626     def _connect2unixsocket(self):
627         """
628         Connect to a unix socket, given its filename
629         :return: boolean
630         """
631         if self.unix_socket is None:
632             self.error("cannot connect to unix socket 'None'")
633             return False
634
635         try:
636             self.debug("attempting DGRAM unix socket '" + str(self.unix_socket) + "'")
637             self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
638             self._sock.connect(self.unix_socket)
639             self.debug("connected DGRAM unix socket '" + str(self.unix_socket) + "'")
640             return True
641         except socket.error as e:
642             self.debug("Failed to connect DGRAM unix socket '" + str(self.unix_socket) + "':", str(e))
643
644         try:
645             self.debug("attempting STREAM unix socket '" + str(self.unix_socket) + "'")
646             self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
647             self._sock.connect(self.unix_socket)
648             self.debug("connected STREAM unix socket '" + str(self.unix_socket) + "'")
649             return True
650         except socket.error as e:
651             self.debug("Failed to connect STREAM unix socket '" + str(self.unix_socket) + "':", str(e))
652             self.error("Failed to connect to unix socket '" + str(self.unix_socket) + "':", str(e))
653             self._sock = None
654             return False
655
656     def _connect(self):
657         """
658         Recreate socket and connect to it since sockets cannot be reused after closing
659         Available configurations are IPv6, IPv4 or UNIX socket
660         :return:
661         """
662         try:
663             if self.unix_socket is not None:
664                 self._connect2unixsocket()
665
666             else:
667                 if self.__socket_config is not None:
668                     self._connect2socket()
669                 else:
670                     for res in socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM):
671                         if self._connect2socket(res): break
672
673         except Exception as e:
674             self._sock = None
675             self.__socket_config = None
676
677         if self._sock is not None:
678             self._sock.setblocking(0)
679             self._sock.settimeout(5)
680             self.debug("set socket timeout to: " + str(self._sock.gettimeout()))
681
682     def _disconnect(self):
683         """
684         Close socket connection
685         :return:
686         """
687         if self._sock is not None:
688             try:
689                 self.debug("closing socket")
690                 self._sock.shutdown(2)  # 0 - read, 1 - write, 2 - all
691                 self._sock.close()
692             except Exception:
693                 pass
694             self._sock = None
695
696     def _send(self):
697         """
698         Send request.
699         :return: boolean
700         """
701         # Send request if it is needed
702         if self.request != self.__empty_request:
703             try:
704                 self.debug("sending request:", str(self.request))
705                 self._sock.send(self.request)
706             except Exception as e:
707                 self._socketerror("error sending request:" + str(e))
708                 self._disconnect()
709                 return False
710         return True
711
712     def _receive(self):
713         """
714         Receive data from socket
715         :return: str
716         """
717         data = ""
718         while True:
719             self.debug("receiving response")
720             try:
721                 buf = self._sock.recv(4096)
722             except Exception as e:
723                 self._socketerror("failed to receive response:" + str(e))
724                 self._disconnect()
725                 break
726
727             if buf is None or len(buf) == 0:  # handle server disconnect
728                 if data == "":
729                     self._socketerror("unexpectedly disconnected")
730                 else:
731                     self.debug("server closed the connection")
732                 self._disconnect()
733                 break
734
735             self.debug("received data:", str(buf))
736             data += buf.decode('utf-8', 'ignore')
737             if self._check_raw_data(data):
738                 break
739
740         self.debug("final response:", str(data))
741         return data
742
743     def _get_raw_data(self):
744         """
745         Get raw data with low-level "socket" module.
746         :return: str
747         """
748         if self._sock is None:
749             self._connect()
750             if self._sock is None:
751                 return None
752
753         # Send request if it is needed
754         if not self._send():
755             return None
756
757         data = self._receive()
758
759         if not self._keep_alive:
760             self._disconnect()
761
762         return data
763
764     def _check_raw_data(self, data):
765         """
766         Check if all data has been gathered from socket
767         :param data: str
768         :return: boolean
769         """
770         return True
771
772     def _parse_config(self):
773         """
774         Parse configuration data
775         :return: boolean
776         """
777         if self.name is None or self.name == str(None):
778             self.name = ""
779         else:
780             self.name = str(self.name)
781
782         try:
783             self.unix_socket = str(self.configuration['socket'])
784         except (KeyError, TypeError):
785             self.debug("No unix socket specified. Trying TCP/IP socket.")
786             self.unix_socket = None
787             try:
788                 self.host = str(self.configuration['host'])
789             except (KeyError, TypeError):
790                 self.debug("No host specified. Using: '" + self.host + "'")
791             try:
792                 self.port = int(self.configuration['port'])
793             except (KeyError, TypeError):
794                 self.debug("No port specified. Using: '" + str(self.port) + "'")
795
796         try:
797             self.request = str(self.configuration['request'])
798         except (KeyError, TypeError):
799             self.debug("No request specified. Using: '" + str(self.request) + "'")
800
801         self.request = self.request.encode()
802
803     def check(self):
804         self._parse_config()
805         return SimpleService.check(self)
806
807
808 class LogService(SimpleService):
809     def __init__(self, configuration=None, name=None):
810         self.log_path = ""
811         self._last_position = 0
812         # self._log_reader = None
813         SimpleService.__init__(self, configuration=configuration, name=name)
814         self.retries = 100000  # basically always retry
815
816     def _get_raw_data(self):
817         """
818         Get log lines since last poll
819         :return: list
820         """
821         lines = []
822         try:
823             if os.path.getsize(self.log_path) < self._last_position:
824                 self._last_position = 0  # read from beginning if file has shrunk
825             elif os.path.getsize(self.log_path) == self._last_position:
826                 self.debug("Log file hasn't changed. No new data.")
827                 return []  # return empty list if nothing has changed
828             with open(self.log_path, "r") as fp:
829                 fp.seek(self._last_position)
830                 for i, line in enumerate(fp):
831                     lines.append(line)
832                 self._last_position = fp.tell()
833         except Exception as e:
834             self.error(str(e))
835
836         if len(lines) != 0:
837             return lines
838         else:
839             self.error("No data collected.")
840             return None
841
842     def check(self):
843         """
844         Parse basic configuration and check if log file exists
845         :return: boolean
846         """
847         if self.name is not None or self.name != str(None):
848             self.name = ""
849         else:
850             self.name = str(self.name)
851         try:
852             self.log_path = str(self.configuration['path'])
853         except (KeyError, TypeError):
854             self.info("No path to log specified. Using: '" + self.log_path + "'")
855
856         if os.access(self.log_path, os.R_OK):
857             return True
858         else:
859             self.error("Cannot access file: '" + self.log_path + "'")
860             return False
861
862     def create(self):
863         # set cursor at last byte of log file
864         self._last_position = os.path.getsize(self.log_path)
865         status = SimpleService.create(self)
866         # self._last_position = 0
867         return status
868
869
870 class ExecutableService(SimpleService):
871
872     def __init__(self, configuration=None, name=None):
873         SimpleService.__init__(self, configuration=configuration, name=name)
874         self.command = None
875
876     def _get_raw_data(self):
877         """
878         Get raw data from executed command
879         :return: <list>
880         """
881         try:
882             p = Popen(self.command, stdout=PIPE, stderr=PIPE)
883         except Exception as error:
884             self.error("Executing command", self.command, "resulted in error:", str(error))
885             return None
886         data = list()
887         for line in p.stdout.readlines():
888             data.append(line.decode())
889
890         return data or None
891
892     def check(self):
893         """
894         Parse basic configuration, check if command is whitelisted and is returning values
895         :return: <boolean>
896         """
897         # Preference: 1. "command" from configuration file 2. "command" from plugin (if specified)
898         if 'command' in self.configuration:
899             self.command = self.configuration['command']
900
901         # "command" must be: 1.not None 2. type <str>
902         if not (self.command and isinstance(self.command, str)):
903             self.error('Command is not defined or command type is not <str>')
904             return False
905
906         # Split "command" into: 1. command <str> 2. options <list>
907         command, opts = self.command.split()[0], self.command.split()[1:]
908
909         # Check for "bad" symbols in options. No pipes, redirects etc. TODO: what is missing?
910         bad_opts = set(''.join(opts)) & set(['&', '|', ';', '>', '<'])
911         if bad_opts:
912             self.error("Bad command argument(s): %s" % bad_opts)
913             return False
914
915         # Find absolute path ('echo' => '/bin/echo')
916         if '/' not in command:
917             command = self.find_binary(command)
918             if not command:
919                 self.error('Can\'t locate "%s" binary in PATH(%s)' % (self.command, PATH))
920                 return False
921         # Check if binary exist and executable
922         else:
923             if not (os.path.isfile(command) and os.access(command, os.X_OK)):
924                 self.error('"%s" is not a file or not executable' % command)
925                 return False
926
927         self.command = [command] + opts if opts else [command]
928
929         try:
930             data = self._get_data()
931         except Exception as error:
932             self.error('_get_data() failed. Command: %s. Error: %s' % (self.command, error))
933             return False
934
935         if isinstance(data, dict) and data:
936             # We need this for create() method. No reason to execute get_data() again if result is not empty dict()
937             self._data_from_check = data
938             return True
939         else:
940             self.error("Command", str(self.command), "returned no data")
941             return False
942
943
944 class MySQLService(SimpleService):
945
946     def __init__(self, configuration=None, name=None):
947         SimpleService.__init__(self, configuration=configuration, name=name)
948         self.__connection = None
949         self.conn_properties = dict()
950         self.queries = dict()
951
952     def __connect(self):
953         try:
954             connection = MySQLdb.connect(connect_timeout=self.update_every, **self.conn_properties)
955         except (MySQLdb.MySQLError, TypeError) as error:
956             return None, str(error)
957         else:
958             return connection, None
959
960     def check(self):
961         def get_connection_properties(conf):
962             properties = dict()
963             if 'user' in conf and conf['user']:
964                 properties['user'] = conf['user']
965             if 'pass' in conf and conf['pass']:
966                 properties['passwd'] = conf['pass']
967             if 'socket' in conf and conf['socket']:
968                 properties['unix_socket'] = conf['socket']
969             elif 'host' in conf and conf['host']:
970                 properties['host'] = conf['host']
971                 properties['port'] = int(conf['port']) if conf.get('port') else 3306
972             elif 'my.cnf' in conf and conf['my.cnf']:
973                 properties['read_default_file'] = conf['my.cnf']
974
975             return properties or None
976
977         def is_valid_queries_dict(raw_queries, log_error):
978             """
979             :param raw_queries: dict:
980             :param log_error: function:
981             :return: dict or None
982
983             raw_queries is valid when: type <dict> and not empty after is_valid_query(for all queries)
984             """
985             def is_valid_query(query):
986                 return all([isinstance(query, str),
987                             query.startswith(('SELECT', 'select', 'SHOW', 'show'))])
988
989             if isinstance(raw_queries, dict) and raw_queries:
990                 valid_queries = dict([(n, q) for n, q in raw_queries.items() if is_valid_query(q)])
991                 bad_queries = set(raw_queries) - set(valid_queries)
992
993                 if bad_queries:
994                     log_error('Removed query(s): %s' % bad_queries)
995                 return valid_queries
996             else:
997                 log_error('Unsupported "queries" format. Must be not empty <dict>')
998                 return None
999
1000         if not PYMYSQL:
1001             self.error('MySQLdb or PyMySQL module is needed to use mysql.chart.py plugin')
1002             return False
1003
1004         # Check if "self.queries" exist, not empty and all queries are in valid format
1005         self.queries = is_valid_queries_dict(self.queries, self.error)
1006         if not self.queries:
1007             return None
1008
1009         # Get connection properties
1010         self.conn_properties = get_connection_properties(self.configuration)
1011         if not self.conn_properties:
1012             self.error('Connection properties are missing')
1013             return False
1014
1015         # Create connection to the database
1016         self.__connection, error = self.__connect()
1017         if error:
1018             self.error('Can\'t establish connection to MySQL: %s' % error)
1019             return False
1020
1021         try:
1022             data = self._get_data() 
1023         except Exception as error:
1024             self.error('_get_data() failed. Error: %s' % error)
1025             return False
1026
1027         if isinstance(data, dict) and data:
1028             # We need this for create() method
1029             self._data_from_check = data
1030             return True
1031         else:
1032             self.error("_get_data() returned no data or type is not <dict>")
1033             return False
1034     
1035     def _get_raw_data(self, description=None):
1036         """
1037         Get raw data from MySQL server
1038         :return: dict: fetchall() or (fetchall(), description)
1039         """
1040
1041         if not self.__connection:
1042             self.__connection, error = self.__connect()
1043             if error:
1044                 return None
1045
1046         raw_data = dict()
1047         try:
1048             with self.__connection as cursor:
1049                 for name, query in self.queries.items():
1050                     try:
1051                         cursor.execute(query)
1052                     except (MySQLdb.ProgrammingError, MySQLdb.OperationalError) as error:
1053                         if exc_info()[0] == MySQLdb.OperationalError and 'denied' not in str(error):
1054                             raise RuntimeError
1055                         self.error('Removed query: %s[%s]. Error: %s'
1056                                    % (name, query, error))
1057                         self.queries.pop(name)
1058                         continue
1059                     else:
1060                         raw_data[name] = (cursor.fetchall(), cursor.description) if description else cursor.fetchall()
1061             self.__connection.commit()
1062         except (MySQLdb.MySQLError, RuntimeError, TypeError, AttributeError):
1063             self.__connection.close()
1064             self.__connection = None
1065             return None
1066         else:
1067             return raw_data or None