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