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