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