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