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