]> arthur.barton.de Git - netdata.git/blob - python.d/python_modules/base.py
Changed ExecutableService check() to split properly
[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         try:
324             print(self._data_stream)
325         except Exception as e:
326             msg.fatal('cannot send data to netdata:', str(e))
327         self._data_stream = ""
328
329     # --- ERROR HANDLING ---
330
331     def error(self, *params):
332         """
333         Show error message on stderr
334         """
335         msg.error(self.chart_name, *params)
336
337     def alert(self, *params):
338         """
339         Show error message on stderr
340         """
341         msg.alert(self.chart_name, *params)
342
343     def debug(self, *params):
344         """
345         Show debug message on stderr
346         """
347         msg.debug(self.chart_name, *params)
348
349     def info(self, *params):
350         """
351         Show information message on stderr
352         """
353         msg.info(self.chart_name, *params)
354
355     # --- MAIN METHODS ---
356
357     def _get_data(self):
358         """
359         Get some data
360         :return: dict
361         """
362         return {}
363
364     def check(self):
365         """
366         check() prototype
367         :return: boolean
368         """
369         self.debug("Module", str(self.__module__), "doesn't implement check() function. Using default.")
370         data = self._get_data()
371
372         if data is None:
373             self.debug("failed to receive data during check().")
374             return False
375
376         if len(data) == 0:
377             self.debug("empty data during check().")
378             return False
379
380         self.debug("successfully received data during check(): '" + str(data) + "'")
381         return True
382
383     def create(self):
384         """
385         Create charts
386         :return: boolean
387         """
388         data = self._get_data()
389         if data is None:
390             self.debug("failed to receive data during create().")
391             return False
392
393         idx = 0
394         for name in self.order:
395             options = self.definitions[name]['options'] + [self.priority + idx, self.update_every]
396             self.chart(self.chart_name + "." + name, *options)
397             # check if server has this datapoint
398             for line in self.definitions[name]['lines']:
399                 if line[0] in data:
400                     self.dimension(*line)
401             idx += 1
402
403         self.commit()
404         return True
405
406     def update(self, interval):
407         """
408         Update charts
409         :param interval: int
410         :return: boolean
411         """
412         data = self._get_data()
413         if data is None:
414             self.debug("failed to receive data during update().")
415             return False
416
417         updated = False
418         for chart in self.order:
419             if self.begin(self.chart_name + "." + chart, interval):
420                 updated = True
421                 for dim in self.definitions[chart]['lines']:
422                     try:
423                         self.set(dim[0], data[dim[0]])
424                     except KeyError:
425                         pass
426                 self.end()
427
428         self.commit()
429         if not updated:
430             self.error("no charts to update")
431
432         return updated
433
434
435 class UrlService(SimpleService):
436     # TODO add support for https connections
437     def __init__(self, configuration=None, name=None):
438         self.url = ""
439         self.user = None
440         self.password = None
441         self.proxies = {}
442         SimpleService.__init__(self, configuration=configuration, name=name)
443
444     def __add_openers(self):
445         # TODO add error handling
446         self.opener = urllib2.build_opener()
447
448         # Proxy handling
449         # TODO currently self.proxies isn't parsed from configuration file
450         # if len(self.proxies) > 0:
451         #     for proxy in self.proxies:
452         #         url = proxy['url']
453         #         # TODO test this:
454         #         if "user" in proxy and "pass" in proxy:
455         #             if url.lower().startswith('https://'):
456         #                 url = 'https://' + proxy['user'] + ':' + proxy['pass'] + '@' + url[8:]
457         #             else:
458         #                 url = 'http://' + proxy['user'] + ':' + proxy['pass'] + '@' + url[7:]
459         #         # FIXME move proxy auth to sth like this:
460         #         #     passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
461         #         #     passman.add_password(None, url, proxy['user'], proxy['password'])
462         #         #     opener.add_handler(urllib2.HTTPBasicAuthHandler(passman))
463         #
464         #         if url.lower().startswith('https://'):
465         #             opener.add_handler(urllib2.ProxyHandler({'https': url}))
466         #         else:
467         #             opener.add_handler(urllib2.ProxyHandler({'https': url}))
468
469         # HTTP Basic Auth
470         if self.user is not None and self.password is not None:
471             passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
472             passman.add_password(None, self.url, self.user, self.password)
473             self.opener.add_handler(urllib2.HTTPBasicAuthHandler(passman))
474             self.debug("Enabling HTTP basic auth")
475
476         #urllib2.install_opener(opener)
477
478     def _get_raw_data(self):
479         """
480         Get raw data from http request
481         :return: str
482         """
483         raw = None
484         try:
485             f = self.opener.open(self.url, timeout=self.update_every * 2)
486             # f = urllib2.urlopen(self.url, timeout=self.update_every * 2)
487         except Exception as e:
488             self.error(str(e))
489             return None
490
491         try:
492             raw = f.read().decode('utf-8', 'ignore')
493         except Exception as e:
494             self.error(str(e))
495         finally:
496             f.close()
497         return raw
498
499     def check(self):
500         """
501         Format configuration data and try to connect to server
502         :return: boolean
503         """
504         if self.name is None or self.name == str(None):
505             self.name = 'local'
506             self.chart_name += "_" + self.name
507         else:
508             self.name = str(self.name)
509         try:
510             self.url = str(self.configuration['url'])
511         except (KeyError, TypeError):
512             pass
513         try:
514             self.user = str(self.configuration['user'])
515         except (KeyError, TypeError):
516             pass
517         try:
518             self.password = str(self.configuration['pass'])
519         except (KeyError, TypeError):
520             pass
521
522         self.__add_openers()
523
524         test = self._get_data()
525         if test is None or len(test) == 0:
526             return False
527         else:
528             return True
529
530
531 class SocketService(SimpleService):
532     def __init__(self, configuration=None, name=None):
533         self._sock = None
534         self._keep_alive = False
535         self.host = "localhost"
536         self.port = None
537         self.unix_socket = None
538         self.request = ""
539         self.__socket_config = None
540         self.__empty_request = "".encode()
541         SimpleService.__init__(self, configuration=configuration, name=name)
542
543     def _socketerror(self, message=None):
544         if self.unix_socket is not None:
545             self.error("unix socket '" + self.unix_socket + "':", message)
546         else:
547             if self.__socket_config is not None:
548                 af, socktype, proto, canonname, sa = self.__socket_config
549                 self.error("socket to '" + str(sa[0]) + "' port " + str(sa[1]) + ":", message)
550             else:
551                 self.error("unknown socket:", message)
552
553     def _connect2socket(self, res=None):
554         """
555         Connect to a socket, passing the result of getaddrinfo()
556         :return: boolean
557         """
558         if res is None:
559             res = self.__socket_config
560             if res is None:
561                 self.error("Cannot create socket to 'None':")
562                 return False
563
564         af, socktype, proto, canonname, sa = res
565         try:
566             self.debug("creating socket to '" + str(sa[0]) + "', port " + str(sa[1]))
567             self._sock = socket.socket(af, socktype, proto)
568         except socket.error as e:
569             self.error("Failed to create socket to '" + str(sa[0]) + "', port " + str(sa[1]) + ":", str(e))
570             self._sock = None
571             self.__socket_config = None
572             return False
573
574         try:
575             self.debug("connecting socket to '" + str(sa[0]) + "', port " + str(sa[1]))
576             self._sock.connect(sa)
577         except socket.error as e:
578             self.error("Failed to connect to '" + str(sa[0]) + "', port " + str(sa[1]) + ":", str(e))
579             self._disconnect()
580             self.__socket_config = None
581             return False
582
583         self.debug("connected to '" + str(sa[0]) + "', port " + str(sa[1]))
584         self.__socket_config = res
585         return True
586
587     def _connect2unixsocket(self):
588         """
589         Connect to a unix socket, given its filename
590         :return: boolean
591         """
592         if self.unix_socket is None:
593             self.error("cannot connect to unix socket 'None'")
594             return False
595
596         try:
597             self.debug("attempting DGRAM unix socket '" + str(self.unix_socket) + "'")
598             self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
599             self._sock.connect(self.unix_socket)
600             self.debug("connected DGRAM unix socket '" + str(self.unix_socket) + "'")
601             return True
602         except socket.error as e:
603             self.debug("Failed to connect DGRAM unix socket '" + str(self.unix_socket) + "':", str(e))
604
605         try:
606             self.debug("attempting STREAM unix socket '" + str(self.unix_socket) + "'")
607             self._sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
608             self._sock.connect(self.unix_socket)
609             self.debug("connected STREAM unix socket '" + str(self.unix_socket) + "'")
610             return True
611         except socket.error as e:
612             self.debug("Failed to connect STREAM unix socket '" + str(self.unix_socket) + "':", str(e))
613             self.error("Failed to connect to unix socket '" + str(self.unix_socket) + "':", str(e))
614             self._sock = None
615             return False
616
617     def _connect(self):
618         """
619         Recreate socket and connect to it since sockets cannot be reused after closing
620         Available configurations are IPv6, IPv4 or UNIX socket
621         :return:
622         """
623         try:
624             if self.unix_socket is not None:
625                 self._connect2unixsocket()
626
627             else:
628                 if self.__socket_config is not None:
629                     self._connect2socket()
630                 else:
631                     for res in socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM):
632                         if self._connect2socket(res): break
633
634         except Exception as e:
635             self._sock = None
636             self.__socket_config = None
637
638         if self._sock is not None:
639             self._sock.setblocking(0)
640             self._sock.settimeout(5)
641             self.debug("set socket timeout to: " + str(self._sock.gettimeout()))
642
643     def _disconnect(self):
644         """
645         Close socket connection
646         :return:
647         """
648         if self._sock is not None:
649             try:
650                 self.debug("closing socket")
651                 self._sock.shutdown(2)  # 0 - read, 1 - write, 2 - all
652                 self._sock.close()
653             except Exception:
654                 pass
655             self._sock = None
656
657     def _send(self):
658         """
659         Send request.
660         :return: boolean
661         """
662         # Send request if it is needed
663         if self.request != self.__empty_request:
664             try:
665                 self.debug("sending request:", str(self.request))
666                 self._sock.send(self.request)
667             except Exception as e:
668                 self._socketerror("error sending request:" + str(e))
669                 self._disconnect()
670                 return False
671         return True
672
673     def _receive(self):
674         """
675         Receive data from socket
676         :return: str
677         """
678         data = ""
679         while True:
680             self.debug("receiving response")
681             try:
682                 buf = self._sock.recv(4096)
683             except Exception as e:
684                 self._socketerror("failed to receive response:" + str(e))
685                 self._disconnect()
686                 break
687
688             if buf is None or len(buf) == 0:  # handle server disconnect
689                 if data == "":
690                     self._socketerror("unexpectedly disconnected")
691                 else:
692                     self.debug("server closed the connection")
693                 self._disconnect()
694                 break
695
696             self.debug("received data:", str(buf))
697             data += buf.decode('utf-8', 'ignore')
698             if self._check_raw_data(data):
699                 break
700
701         self.debug("final response:", str(data))
702         return data
703
704     def _get_raw_data(self):
705         """
706         Get raw data with low-level "socket" module.
707         :return: str
708         """
709         if self._sock is None:
710             self._connect()
711             if self._sock is None:
712                 return None
713
714         # Send request if it is needed
715         if not self._send():
716             return None
717
718         data = self._receive()
719
720         if not self._keep_alive:
721             self._disconnect()
722
723         return data
724
725     def _check_raw_data(self, data):
726         """
727         Check if all data has been gathered from socket
728         :param data: str
729         :return: boolean
730         """
731         return True
732
733     def _parse_config(self):
734         """
735         Parse configuration data
736         :return: boolean
737         """
738         if self.name is None or self.name == str(None):
739             self.name = ""
740         else:
741             self.name = str(self.name)
742
743         try:
744             self.unix_socket = str(self.configuration['socket'])
745         except (KeyError, TypeError):
746             self.debug("No unix socket specified. Trying TCP/IP socket.")
747             self.unix_socket = None
748             try:
749                 self.host = str(self.configuration['host'])
750             except (KeyError, TypeError):
751                 self.debug("No host specified. Using: '" + self.host + "'")
752             try:
753                 self.port = int(self.configuration['port'])
754             except (KeyError, TypeError):
755                 self.debug("No port specified. Using: '" + str(self.port) + "'")
756
757         try:
758             self.request = str(self.configuration['request'])
759         except (KeyError, TypeError):
760             self.debug("No request specified. Using: '" + str(self.request) + "'")
761
762         self.request = self.request.encode()
763
764     def check(self):
765         self._parse_config()
766         return SimpleService.check(self)
767
768
769 class LogService(SimpleService):
770     def __init__(self, configuration=None, name=None):
771         self.log_path = ""
772         self._last_position = 0
773         # self._log_reader = None
774         SimpleService.__init__(self, configuration=configuration, name=name)
775         self.retries = 100000  # basically always retry
776
777     def _get_raw_data(self):
778         """
779         Get log lines since last poll
780         :return: list
781         """
782         lines = []
783         try:
784             if os.path.getsize(self.log_path) < self._last_position:
785                 self._last_position = 0  # read from beginning if file has shrunk
786             elif os.path.getsize(self.log_path) == self._last_position:
787                 self.debug("Log file hasn't changed. No new data.")
788                 return []  # return empty list if nothing has changed
789             with open(self.log_path, "r") as fp:
790                 fp.seek(self._last_position)
791                 for i, line in enumerate(fp):
792                     lines.append(line)
793                 self._last_position = fp.tell()
794         except Exception as e:
795             self.error(str(e))
796
797         if len(lines) != 0:
798             return lines
799         else:
800             self.error("No data collected.")
801             return None
802
803     def check(self):
804         """
805         Parse basic configuration and check if log file exists
806         :return: boolean
807         """
808         if self.name is not None or self.name != str(None):
809             self.name = ""
810         else:
811             self.name = str(self.name)
812         try:
813             self.log_path = str(self.configuration['path'])
814         except (KeyError, TypeError):
815             self.info("No path to log specified. Using: '" + self.log_path + "'")
816
817         if os.access(self.log_path, os.R_OK):
818             return True
819         else:
820             self.error("Cannot access file: '" + self.log_path + "'")
821             return False
822
823     def create(self):
824         # set cursor at last byte of log file
825         self._last_position = os.path.getsize(self.log_path)
826         status = SimpleService.create(self)
827         # self._last_position = 0
828         return status
829
830
831 class ExecutableService(SimpleService):
832     bad_substrings = ('&', '|', ';', '>', '<')
833
834     def __init__(self, configuration=None, name=None):
835         self.command = ""
836         SimpleService.__init__(self, configuration=configuration, name=name)
837
838     def _get_raw_data(self):
839         """
840         Get raw data from executed command
841         :return: str
842         """
843         try:
844             p = Popen(self.command, stdout=PIPE, stderr=PIPE)
845         except Exception as e:
846             self.error("Executing command", self.command, "resulted in error:", str(e))
847             return None
848         data = []
849         for line in p.stdout.readlines():
850             data.append(str(line.decode()))
851
852         if len(data) == 0:
853             self.error("No data collected.")
854             return None
855
856         return data
857
858     def check(self):
859         """
860         Parse basic configuration, check if command is whitelisted and is returning values
861         :return: boolean
862         """
863         if self.name is not None or self.name != str(None):
864             self.name = ""
865         else:
866             self.name = str(self.name)
867         try:
868             self.command = str(self.configuration['command'])
869         except (KeyError, TypeError):
870             self.info("No command specified. Using: '" + self.command + "'")
871         # Splitting self.command on every space so subprocess.Popen reads it properly
872         self.command = self.command.split(' ')
873
874         for arg in self.command[1:]:
875             if any(st in arg for st in self.bad_substrings):
876                 self.error("Bad command argument:" + " ".join(self.command[1:]))
877                 return False
878
879         # test command and search for it in /usr/sbin or /sbin when failed
880         base = self.command[0].split('/')[-1]
881         if self._get_raw_data() is None:
882             for prefix in ['/sbin/', '/usr/sbin/']:
883                 self.command[0] = prefix + base
884                 if os.path.isfile(self.command[0]):
885                     break
886
887         if self._get_data() is None or len(self._get_data()) == 0:
888             self.error("Command", self.command, "returned no data")
889             return False
890
891         return True