8 assert sys.version_info >= (3, 1)
9 import importlib.machinery
11 # change this hack below if we want PY_VERSION to be used in modules
13 # builtins.PY_VERSION = 3
15 sys.stderr.write('python.d.plugin: Using python 3\n')
16 except (AssertionError, ImportError):
20 # change this hack below if we want PY_VERSION to be used in modules
22 # __builtin__.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: Cannot start. No importlib.machinery on python3 or lack of imp on python2\n')
27 sys.stdout.write('DISABLE\n')
32 sys.stderr.write('python.d.plugin: Cannot find yaml library\n')
33 sys.stdout.write('DISABLE\n')
37 PROGRAM = "python.d.plugin"
38 MODULE_EXTENSION = ".chart.py"
39 BASE_CONFIG = {'update_every': 10,
44 class PythonCharts(object):
46 Main class used to control every python module.
51 modules_path='../python.d/',
52 modules_configs='../conf.d/',
53 modules_disabled=None):
57 :param modules_path: str
58 :param modules_configs: str
59 :param modules_disabled: list
64 if modules_disabled is None:
68 # set configuration directory
69 self.configs = modules_configs
72 loaded_modules = self._load_modules(modules_path, modules, modules_disabled)
74 # load configuration files
75 configured_modules = self._load_configs(loaded_modules)
77 # good economy and prosperity:
78 self.jobs = self._create_jobs(configured_modules) # type: list
79 if DEBUG_FLAG and interval is not None:
81 job.create_timetable(interval)
84 def _import_module(path, name=None):
86 Try to import module using only its path.
93 name = path.split('/')[-1]
94 if name[-len(MODULE_EXTENSION):] != MODULE_EXTENSION:
96 name = name[:-len(MODULE_EXTENSION)]
99 return importlib.machinery.SourceFileLoader(name, path).load_module()
101 return imp.load_source(name, path)
102 except Exception as e:
106 def _load_modules(self, path, modules, disabled):
108 Load modules from 'modules' list or dynamically every file from 'path' (only .chart.py files)
111 :param disabled: list
115 # check if plugin directory exists
116 if not os.path.isdir(path):
117 debug("cannot find charts directory ", path)
118 sys.stdout.write("DISABLE\n")
127 mod = self._import_module(path + m + MODULE_EXTENSION)
130 else: # exit if plugin is not found
131 sys.stdout.write("DISABLE")
135 # scan directory specified in path and load all modules from there
136 names = os.listdir(path)
138 if mod.strip(MODULE_EXTENSION) in disabled:
139 debug("disabling:", mod.strip(MODULE_EXTENSION))
141 m = self._import_module(path + mod)
143 debug("loading module: '" + path + mod + "'")
147 def _load_configs(self, modules):
149 Append configuration in list named `config` to every module.
150 For multi-job modules `config` list is created in _parse_config,
151 otherwise it is created here based on BASE_CONFIG prototype with None as identifier.
156 configfile = self.configs + mod.__name__ + ".conf"
157 if os.path.isfile(configfile):
158 debug("loading module configuration: '" + configfile + "'")
162 self._parse_config(mod, read_config(configfile)))
163 except Exception as e:
164 debug("something went wrong while loading configuration", e)
167 ": configuration file '" +
169 "' not found. Using defaults.")
170 # set config if not found
171 if not hasattr(mod, 'config'):
172 mod.config = {None: {}}
173 for var in BASE_CONFIG:
175 mod.config[None][var] = getattr(mod, var)
176 except AttributeError:
177 mod.config[None][var] = BASE_CONFIG[var]
181 def _parse_config(module, config):
183 Parse configuration file or extract configuration from module file.
184 Example of returned dictionary:
190 :param module: object
196 for key in BASE_CONFIG:
198 # get defaults from module config
199 defaults[key] = int(config.pop(key))
200 except (KeyError, ValueError):
202 # get defaults from module source code
203 defaults[key] = getattr(module, key)
204 except (KeyError, ValueError):
205 # if above failed, get defaults from global dict
206 defaults[key] = BASE_CONFIG[key]
208 # check if there are dict in config dict
211 if type(config[name]) is dict:
215 # assign variables needed by supervisor to every job configuration
219 if key not in config[name]:
220 config[name][key] = defaults[key]
221 # if only one job is needed, values doesn't have to be in dict (in YAML)
223 config = {None: config.copy()}
224 config[None].update(defaults)
226 # return dictionary of jobs where every job has BASE_CONFIG variables
230 def _create_jobs(modules):
232 Create jobs based on module.config dictionary and module.Service class definition.
237 for module in modules:
238 for name in module.config:
240 conf = module.config[name]
242 job = module.Service(configuration=conf, name=name)
243 except Exception as e:
244 debug(module.__name__ +
245 ": Couldn't start job named " +
251 # set chart_name (needed to plot run time graphs)
252 job.chart_name = module.__name__
254 job.chart_name += "_" + name
257 return [j for j in jobs if j is not None]
259 def _stop(self, job, reason=None):
261 Stop specified job and remove it from self.jobs list
262 Also notifies user about job failure if DEBUG_FLAG is set
267 if job.name is not None:
268 prefix = "'" + job.name + "' in "
270 prefix += "'" + job.__module__ + MODULE_EXTENSION + "' "
271 self.jobs.remove(job)
274 elif reason[:3] == "no ":
276 "does not seem to have " +
278 "() function. Disabling it.")
279 elif reason[:7] == "failed ":
282 "() function reports failure.")
283 elif reason[:13] == "configuration":
285 "configuration file '" +
288 ".conf' not found. Using defaults.")
289 elif reason[:11] == "misbehaving":
290 debug(prefix + "is " + reason)
294 Tries to execute check() on every job.
295 This cannot fail thus it is catching every exception
296 If job.check() fails job is stopped
299 while i < len(self.jobs):
303 self._stop(job, "failed check")
306 except AttributeError:
307 self._stop(job, "no check")
308 except (UnboundLocalError, Exception) as e:
309 self._stop(job, "misbehaving. Reason: " + str(e))
313 Tries to execute create() on every job.
314 This cannot fail thus it is catching every exception.
315 If job.create() fails job is stopped.
316 This is also creating job run time chart.
319 while i < len(self.jobs):
323 self._stop(job, "failed create")
325 chart = job.chart_name
327 "CHART netdata.plugin_pythond_" +
329 " '' 'Execution time for " +
331 " plugin' 'milliseconds / run' python.d netdata.plugin_python area 145000 " +
332 str(job.timetable['freq']) +
334 sys.stdout.write("DIMENSION run_time 'run time' absolute 1 1\n\n")
337 except AttributeError:
338 self._stop(job, "no create")
339 except (UnboundLocalError, Exception) as e:
340 self._stop(job, "misbehaving. Reason: " + str(e))
342 def _update_job(self, job):
344 Tries to execute update() on specified job.
345 This cannot fail thus it is catching every exception.
346 If job.update() returns False, number of retries_left is decremented.
347 If there are no more retries, job is stopped.
348 Job is also stopped if it throws an exception.
349 This is also updating job run time chart.
350 Return False if job is stopped
354 t_start = time.time()
355 # check if it is time to execute job update() function
356 if job.timetable['next'] > t_start:
362 since_last = int((t_start - job.timetable['last']) * 1000000)
363 if not job.update(since_last):
364 if job.retries_left <= 0:
365 self._stop(job, "update failed")
367 job.retries_left -= 1
368 job.timetable['next'] += job.timetable['freq']
370 except AttributeError:
371 self._stop(job, "no update")
373 except (UnboundLocalError, Exception) as e:
374 self._stop(job, "misbehaving. Reason: " + str(e))
377 job.timetable['next'] = t_end - (t_end % job.timetable['freq']) + job.timetable['freq']
378 # draw performance graph
379 sys.stdout.write("BEGIN netdata.plugin_pythond_" + job.chart_name + " " + str(since_last) + '\n')
380 sys.stdout.write("SET run_time = " + str(int((t_end - t_start) * 1000)) + '\n')
381 sys.stdout.write("END\n")
383 job.timetable['last'] = t_start
384 job.retries_left = job.retries
385 self.first_run = False
390 Tries to execute update() on every job by using _update_job()
391 This will stay forever and ever and ever forever and ever it'll be the one...
393 self.first_run = True
397 while i < len(self.jobs):
399 if self._update_job(job):
401 next_runs.append(job.timetable['next'])
405 if len(next_runs) == 0:
406 debug("No plugins loaded")
407 sys.stdout.write("DISABLE\n")
409 time.sleep(min(next_runs) - time.time())
412 def read_config(path):
414 Read YAML configuration from specified file
419 with open(path, 'r') as stream:
420 config = yaml.load(stream)
421 except (OSError, IOError):
422 debug(str(path), "is not a valid configuration file")
424 except yaml.YAMLError as e:
425 debug(str(path), "is malformed:", e)
432 Print message on stderr.
436 sys.stderr.write(PROGRAM + ":")
438 sys.stderr.write(" " + str(i))
439 sys.stderr.write("\n")
443 def parse_cmdline(directory, *commands):
445 Parse parameters from command line.
446 :param directory: str
447 :param commands: list of str
454 for cmd in commands[1:]:
457 elif cmd == "debug" or cmd == "all":
459 # redirect stderr to stdout?
460 elif os.path.isfile(directory + cmd + ".chart.py") or os.path.isfile(directory + cmd):
462 mods.append(cmd.replace(".chart.py", ""))
470 debug("started from", commands[0], "with options:", *commands[1:])
471 if len(mods) == 0 and DEBUG_FLAG is False:
474 return {'interval': interval,
478 # if __name__ == '__main__':
483 global PROGRAM, DEBUG_FLAG
484 PROGRAM = sys.argv[0].split('/')[-1].split('.plugin')[0]
485 # parse env variables
486 # https://github.com/firehol/netdata/wiki/External-Plugins#environment-variables
487 main_dir = os.getenv('NETDATA_PLUGINS_DIR',
488 os.path.abspath(__file__).strip("python.d.plugin.py"))
489 config_dir = os.getenv('NETDATA_CONFIG_DIR', "/etc/netdata/")
490 interval = os.getenv('NETDATA_UPDATE_EVERY', None)
492 # read configuration file
494 if config_dir[-1] != '/':
496 configfile = config_dir + "python.d.conf"
498 conf = read_config(configfile)
501 if str(conf['enable']) is False:
502 debug("disabled in configuration file")
503 sys.stdout.write("DISABLE\n")
505 except (KeyError, TypeError):
508 modules_conf = conf['plugins_config_dir']
509 except (KeyError, TypeError):
510 modules_conf = config_dir + "python.d/" # default configuration directory
512 modules_dir = conf['plugins_dir']
513 except (KeyError, TypeError):
514 modules_dir = main_dir.replace("plugins.d", "python.d")
516 interval = conf['interval']
517 except (KeyError, TypeError):
518 pass # use default interval from NETDATA_UPDATE_EVERY
520 DEBUG_FLAG = conf['debug']
521 except (KeyError, TypeError):
523 for k, v in conf.items():
524 if k in ("plugins_config_dir", "plugins_dir", "interval", "debug"):
529 modules_conf = config_dir + "python.d/"
530 modules_dir = main_dir.replace("plugins.d", "python.d")
532 # directories should end with '/'
533 if modules_dir[-1] != '/':
535 if modules_conf[-1] != '/':
538 # parse passed command line arguments
539 out = parse_cmdline(modules_dir, *sys.argv)
540 modules = out['modules']
541 if out['interval'] is not None:
542 interval = out['interval']
544 # configure environment to run modules
545 sys.path.append(modules_dir + "python_modules") # append path to directory with modules dependencies
548 charts = PythonCharts(interval, modules, modules_dir, modules_conf, disabled)
552 sys.stdout.write("DISABLE")
555 if __name__ == '__main__':