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