]> arthur.barton.de Git - netdata.git/blob - plugins.d/python.d.plugin
418ea91263ca5a2ad9ae0f969972ee41b5328016
[netdata.git] / plugins.d / python.d.plugin
1 #!/usr/bin/env python3
2
3 import os
4 import sys
5 import time
6 try:
7     assert sys.version_info >= (3,1)
8     import importlib.machinery
9 except AssertionError:
10     sys.stderr.write('python.d.plugin: Not supported python version. Needed python >= 3.1\n')
11     sys.stdout.write('DISABLE\n')
12     sys.exit(1)
13 try:
14     import yaml
15 except ImportError:
16     sys.stderr.write('python.d.plugin: Cannot find yaml library\n')
17     sys.stdout.write('DISABLE\n')
18     sys.exit(1)
19
20 BASE_CONFIG = {'update_every' : 10,
21                'priority': 12345,
22                'retries' : 0}
23
24
25 class PythonCharts(object):
26     
27     def __init__(self,
28                  interval=None,
29                  modules=[],
30                  modules_path='../python.d/',
31                  modules_configs='../conf.d/',
32                  modules_disabled=[]):
33         self.first_run = True
34         # set configuration directory
35         self.configs = modules_configs
36
37         # load modules
38         modules = self._load_modules(modules_path,modules)
39         
40         # check if loaded modules are on disabled modules list
41         loaded_modules = [ m for m in modules if m.__name__ not in modules_disabled ]
42         
43         # load configuration files
44         configured_modules = self._load_configs(loaded_modules)
45
46         # good economy and prosperity:
47         self.jobs = self._create_jobs(configured_modules)
48         if DEBUG_FLAG and interval is not None:
49             for job in self.jobs:
50                 job._create_timetable(interval)
51
52
53     def _create_jobs(self,modules):
54     # module store a definition of Service class
55     # module store configuration in module.config
56     # configs are list of dicts or a dict
57     # one dict is one service
58     # iterate over list of modules and inside one loop iterate over configs
59         jobs = []
60         for module in modules:
61             for name in module.config:
62                 # register a new job
63                 conf = module.config[name]
64                 try:
65                     job = module.Service(configuration=conf, name=name)
66                 except Exception as e:
67                     debug(module.__name__ +
68                           ": Couldn't start job named " +
69                           str(name) + 
70                           ": " +
71                           str(e))
72                     return None
73                 else:
74                     # set execution_name (needed to plot run time graphs)
75                     job.execution_name = module.__name__
76                     if name is not None:
77                         job.execution_name += "_" + name
78                 jobs.append(job)
79         
80         return [j for j in jobs if j is not None]
81
82     def _import_module(self, path, name=None):
83     # try to import module using only its path
84         if name is None:
85             name = path.split('/')[-1]
86             if name[-9:] != ".chart.py":
87                 return None
88             name = name[:-9]
89         try:
90             return importlib.machinery.SourceFileLoader(name, path).load_module()
91         except Exception as e:
92             debug(str(e))
93             return None
94
95     def _load_modules(self, path, modules):
96         # check if plugin directory exists
97         if not os.path.isdir(path):
98             debug("cannot find charts directory ", path)
99             sys.stdout.write("DISABLE\n")
100             sys.exit(1)
101
102         # load modules
103         loaded = []
104         if len(modules) > 0:
105             for m in modules:
106                 mod = self._import_module(path + m + ".chart.py")
107                 if mod is not None:
108                     loaded.append(mod)
109         else:
110             # scan directory specified in path and load all modules from there
111             names = os.listdir(path)
112             for mod in names:
113                 m = self._import_module(path + mod)
114                 if m is not None:
115                     debug("loading chart: '" + path + mod + "'")
116                     loaded.append(m)
117         return loaded
118
119     def _load_configs(self,modules):
120     # function loads configuration files to modules
121         for mod in modules:
122             configfile = self.configs + mod.__name__ + ".conf"
123             if os.path.isfile(configfile):
124                 debug("loading chart options: '" + configfile + "'")
125                 try:
126                     setattr(mod,
127                             'config',
128                             self._parse_config(mod,read_config(configfile)))
129                 except Exception as e:
130                     debug("something went wrong while loading configuration",e)
131             else:
132                 debug(mod.__name__ +
133                       ": configuration file '" +
134                       configfile +
135                       "' not found. Using defaults.")
136         return modules
137
138     def _parse_config(self,module,config):
139         # get default values
140         defaults = {}
141         for key in BASE_CONFIG:
142             try:
143                 # get defaults from module config
144                 defaults[key] = int(config.pop(key))
145             except (KeyError,ValueError):
146                 try:
147                     # get defaults from module source code
148                     defaults[key] = getattr(module, key)
149                 except (KeyError,ValueError):
150                     # if above failed, get defaults from global dict
151                     defaults[key] = BASE_CONFIG[key]
152       
153         # check if there are dict in config dict
154         many_jobs = False
155         for name in config:
156             if type(config[name]) is dict:
157                 many_jobs = True
158                 break
159         
160         # assign variables needed by supervisor to every job configuration
161         if many_jobs:
162             for name in config:
163                 for key in defaults:
164                     if key not in config[name]:
165                         config[name][key] = defaults[key]
166         # if only one job is needed, values doesn't have to be in dict (in YAML)
167         else:
168             config = {None: config.copy()}
169             config[None].update(defaults)
170             
171         # return dictionary of jobs where every job has BASE_CONFIG variables
172         return config
173
174     def _stop(self, job, reason=None): #FIXME test if Service has __name__
175     # modifies self.jobs
176         self.jobs.remove(job)
177         if reason is None:
178             return
179         elif reason[:3] == "no ":
180             debug("chart '" +
181                   job.execution_name,
182                   "' does not seem to have " +
183                   reason[3:] +
184                   "() function. Disabling it.")
185         elif reason[:7] == "failed ":
186             debug("chart '" +
187                   job.execution_name + "' " +
188                   reason[7:] +
189                   "() function reports failure.")
190         elif reason[:13] == "configuration":
191             debug(job.execution_name,
192                   "configuration file '" +
193                   self.configs +
194                   job.execution_name +
195                   ".conf' not found. Using defaults.")
196         elif reason[:11] == "misbehaving":
197             debug(job.execution_name, "is "+reason)
198
199     def check(self):
200     # try to execute check() on every job
201         for job in self.jobs:
202             try:
203                 if not job.check():
204                     self._stop(job, "failed check")
205             except AttributeError:
206                 self._stop(job, "no check")
207             except (UnboundLocalError, Exception) as e:
208                 self._stop(job, "misbehaving. Reason: " + str(e))
209         print("CHECK OK")
210
211     def create(self):
212     # try to execute create() on every job
213         for job in self.jobs:
214             try:
215                 if not job.create():
216                     self._stop(job, "failed create")
217                 else:
218                     chart = job.execution_name
219                     sys.stdout.write(
220                         "CHART netdata.plugin_pythond_" +
221                         chart +
222                         " '' 'Execution time for " +
223                         chart +
224                         " plugin' 'milliseconds / run' python.d netdata.plugin_python area 145000 " +
225                         str(job.timetable['freq']) +
226                         '\n')
227                     sys.stdout.write("DIMENSION run_time 'run time' absolute 1 1\n\n")
228                     sys.stdout.flush()
229             except AttributeError:
230                 self._stop(job, "no create")
231             except (UnboundLocalError, Exception) as e:
232                 self._stop(job, "misbehaving. Reason: " + str(e))
233         print("CREATE OK")
234
235     def _update_job(self, job):
236     # try to execute update() on every job and draw run time graph 
237         t_start = time.time()
238         # check if it is time to execute job update() function
239         if job.timetable['next'] > t_start:
240             return
241         try:
242             if self.first_run:
243                 since_last = 0
244             else:
245                 since_last = int((t_start - job.timetable['last']) * 1000000)
246             if not job.update(since_last):
247                 self._stop(job, "update failed")
248                 return
249         except AttributeError:
250             self._stop(job, "no update")
251             return
252         except (UnboundLocalError, Exception) as e:
253             self._stop(job, "misbehaving. Reason: " + str(e))
254             return
255         t_end = time.time()
256         job.timetable['next'] = t_end - (t_end % job.timetable['freq']) + job.timetable['freq']
257         # draw performance graph
258         if self.first_run:
259             dt = 0
260         else:
261             dt = int((t_end - job.timetable['last']) * 1000000)
262         sys.stdout.write("BEGIN netdata.plugin_pythond_"+job.execution_name+" "+str(since_last)+'\n')
263         sys.stdout.write("SET run_time = " + str(int((t_end - t_start) * 1000)) + '\n')
264         sys.stdout.write("END\n")
265         sys.stdout.flush()
266         job.timetable['last'] = t_start
267         self.first_run = False
268
269     def update(self):
270     # run updates (this will stay forever and ever and ever forever and ever it'll be the one...)
271         self.first_run = True
272         while True:
273             t_begin = time.time()
274             next_runs = []
275             for job in self.jobs:
276                 self._update_job(job)
277                 try:
278                     next_runs.append(job.timetable['next'])
279                 except KeyError:
280                     pass
281             if len(next_runs) == 0:
282                 debug("No plugins loaded")
283                 sys.stdout.write("DISABLE\n")
284                 sys.exit(1)
285             time.sleep(min(next_runs) - time.time())
286
287
288 def read_config(path):
289     try:
290         with open(path, 'r') as stream:
291             config = yaml.load(stream)
292     except IsADirectoryError:
293         debug(str(path), "is a directory")
294         return None
295     except yaml.YAMLError as e:
296         debug(str(path), "is malformed:", e)
297         return None
298     return config
299
300
301 def debug(*args):
302     if not DEBUG_FLAG:
303         return
304     sys.stderr.write(PROGRAM + ":")
305     for i in args:
306         sys.stderr.write(" " + str(i))
307     sys.stderr.write("\n")
308     sys.stderr.flush()
309
310
311 def parse_cmdline(directory, *commands):
312     # TODO number -> interval
313     global DEBUG_FLAG
314     DEBUG_FLAG = False
315     interval = None
316
317     mods = []
318     for cmd in commands[1:]:
319         if cmd == "check":
320             pass
321         elif cmd == "debug" or cmd == "all":
322             DEBUG_FLAG = True
323             # redirect stderr to stdout?
324         elif os.path.isfile(directory + cmd + ".chart.py") or os.path.isfile(directory + cmd):
325             DEBUG_FLAG = True
326             mods.append(cmd.replace(".chart.py", ""))
327         else:
328             try:
329                 interval = int(cmd)
330             except ValueError:
331                 pass
332
333     debug("started from", commands[0], "with options:", *commands[1:])
334     if len(mods) == 0 and DEBUG_FLAG is False:
335         interval = None
336
337     return {'interval': interval,
338             'modules': mods}
339
340
341 # if __name__ == '__main__':
342 def run():
343     global DEBUG_FLAG, PROGRAM
344     DEBUG_FLAG = True
345     PROGRAM = sys.argv[0].split('/')[-1].split('.plugin')[0]
346     # parse env variables
347     # https://github.com/firehol/netdata/wiki/External-Plugins#environment-variables
348     main_dir = os.getenv('NETDATA_PLUGINS_DIR',
349                          os.path.abspath(__file__).strip("python.d.plugin.py"))
350     config_dir = os.getenv('NETDATA_CONFIG_DIR', "/etc/netdata/")
351     interval = os.getenv('NETDATA_UPDATE_EVERY', None)
352
353     # read configuration file
354     disabled = []
355     if config_dir[-1] != '/':
356         config_dir += '/'
357     configfile = config_dir + "python.d.conf"
358
359     try:
360         conf = read_config(configfile)
361         try:
362             if str(conf['enable']) is False:
363                 debug("disabled in configuration file")
364                 sys.stdout.write("DISABLE\n")
365                 sys.exit(1)
366         except (KeyError, TypeError):
367             pass
368         try:
369             modules_conf = conf['plugins_config_dir']
370         except KeyError:
371             modules_conf = config_dir + "python.d/"  # default configuration directory
372         try:
373             modules_dir = conf['plugins_dir']
374         except KeyError:
375             modules_dir = main_dir.replace("plugins.d", "python.d")
376         try:
377             interval = conf['interval']
378         except KeyError:
379             pass  # use default interval from NETDATA_UPDATE_EVERY
380         try:
381             DEBUG_FLAG = conf['debug']
382         except KeyError:
383             pass
384         for k, v in conf.items():
385             if k in ("plugins_config_dir", "plugins_dir", "interval", "debug"):
386                 continue
387             if v is False:
388                 disabled.append(k)
389     except FileNotFoundError:
390         modules_conf = config_dir
391         modules_dir = main_dir.replace("plugins.d", "python.d")
392
393     # directories should end with '/'
394     if modules_dir[-1] != '/':
395         modules_dir += "/"
396     if modules_conf[-1] != '/':
397         modules_conf += "/"
398
399     # parse passed command line arguments
400     out = parse_cmdline(modules_dir, *sys.argv)
401     modules = out['modules']
402     if out['interval'] is not None:
403         interval = out['interval']
404     
405     # configure environement to run modules
406     sys.path.append(modules_dir+"python_modules") # append path to directory with modules dependencies
407     
408     # run plugins
409     charts = PythonCharts(interval, modules, modules_dir, modules_conf, disabled)
410     charts.check()
411     charts.create()
412     charts.update()
413
414 if __name__ == '__main__':
415     run()