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