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