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 chart: '" + 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 chart options: '" + 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 execution_name (needed to plot run time graphs)
252 job.execution_name = module.__name__
254 job.execution_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
266 self.jobs.remove(job)
269 elif reason[:3] == "no ":
272 "' does not seem to have " +
274 "() function. Disabling it.")
275 elif reason[:7] == "failed ":
277 job.execution_name + "' " +
279 "() function reports failure.")
280 elif reason[:13] == "configuration":
281 debug(job.execution_name,
282 "configuration file '" +
285 ".conf' not found. Using defaults.")
286 elif reason[:11] == "misbehaving":
287 debug(job.execution_name, "is " + reason)
291 Tries to execute check() on every job.
292 This cannot fail thus it is catching every exception
293 If job.check() fails job is stopped
295 for job in self.jobs:
298 self._stop(job, "failed check")
299 except AttributeError:
300 self._stop(job, "no check")
301 except (UnboundLocalError, Exception) as e:
302 self._stop(job, "misbehaving. Reason: " + str(e))
306 Tries to execute create() on every job.
307 This cannot fail thus it is catching every exception.
308 If job.create() fails job is stopped.
309 This is also creating job run time chart.
311 for job in self.jobs:
314 self._stop(job, "failed create")
316 chart = job.execution_name
318 "CHART netdata.plugin_pythond_" +
320 " '' 'Execution time for " +
322 " plugin' 'milliseconds / run' python.d netdata.plugin_python area 145000 " +
323 str(job.timetable['freq']) +
325 sys.stdout.write("DIMENSION run_time 'run time' absolute 1 1\n\n")
327 except AttributeError:
328 self._stop(job, "no create")
329 except (UnboundLocalError, Exception) as e:
330 self._stop(job, "misbehaving. Reason: " + str(e))
332 def _update_job(self, job):
334 Tries to execute update() on specified job.
335 This cannot fail thus it is catching every exception.
336 If job.update() returns False, number of retries_left is decremented.
337 If there are no more retries, job is stopped.
338 Job is also stopped if it throws an exception.
339 This is also updating job run time chart.
342 t_start = time.time()
343 # check if it is time to execute job update() function
344 if job.timetable['next'] > t_start:
350 since_last = int((t_start - job.timetable['last']) * 1000000)
351 if not job.update(since_last):
352 if job.retries_left <= 0:
353 self._stop(job, "update failed")
354 job.retries_left -= 1
355 job.timetable['next'] += job.timetable['freq']
357 except AttributeError:
358 self._stop(job, "no update")
360 except (UnboundLocalError, Exception) as e:
361 self._stop(job, "misbehaving. Reason: " + str(e))
364 job.timetable['next'] = t_end - (t_end % job.timetable['freq']) + job.timetable['freq']
365 # draw performance graph
366 sys.stdout.write("BEGIN netdata.plugin_pythond_" + job.execution_name + " " + str(since_last) + '\n')
367 sys.stdout.write("SET run_time = " + str(int((t_end - t_start) * 1000)) + '\n')
368 sys.stdout.write("END\n")
370 job.timetable['last'] = t_start
371 job.retries_left = job.retries
372 self.first_run = False
376 Tries to execute update() on every job by using _update_job()
377 This will stay forever and ever and ever forever and ever it'll be the one...
379 self.first_run = True
382 for job in self.jobs:
383 self._update_job(job)
385 next_runs.append(job.timetable['next'])
388 if len(next_runs) == 0:
389 debug("No plugins loaded")
390 sys.stdout.write("DISABLE\n")
392 time.sleep(min(next_runs) - time.time())
395 def read_config(path):
397 Read YAML configuration from specified file
402 with open(path, 'r') as stream:
403 config = yaml.load(stream)
404 except (OSError, IOError):
405 debug(str(path), "is not a valid configuration file")
407 except yaml.YAMLError as e:
408 debug(str(path), "is malformed:", e)
415 Print message on stderr.
419 sys.stderr.write(PROGRAM + ":")
421 sys.stderr.write(" " + str(i))
422 sys.stderr.write("\n")
426 def parse_cmdline(directory, *commands):
428 Parse parameters from command line.
429 :param directory: str
430 :param commands: list of str
437 for cmd in commands[1:]:
440 elif cmd == "debug" or cmd == "all":
442 # redirect stderr to stdout?
443 elif os.path.isfile(directory + cmd + ".chart.py") or os.path.isfile(directory + cmd):
445 mods.append(cmd.replace(".chart.py", ""))
453 debug("started from", commands[0], "with options:", *commands[1:])
454 if len(mods) == 0 and DEBUG_FLAG is False:
457 return {'interval': interval,
461 # if __name__ == '__main__':
466 global PROGRAM, DEBUG_FLAG
467 PROGRAM = sys.argv[0].split('/')[-1].split('.plugin')[0]
468 # parse env variables
469 # https://github.com/firehol/netdata/wiki/External-Plugins#environment-variables
470 main_dir = os.getenv('NETDATA_PLUGINS_DIR',
471 os.path.abspath(__file__).strip("python.d.plugin.py"))
472 config_dir = os.getenv('NETDATA_CONFIG_DIR', "/etc/netdata/")
473 interval = os.getenv('NETDATA_UPDATE_EVERY', None)
475 # read configuration file
477 if config_dir[-1] != '/':
479 configfile = config_dir + "python.d.conf"
481 conf = read_config(configfile)
484 if str(conf['enable']) is False:
485 debug("disabled in configuration file")
486 sys.stdout.write("DISABLE\n")
488 except (KeyError, TypeError):
491 modules_conf = conf['plugins_config_dir']
492 except (KeyError, TypeError):
493 modules_conf = config_dir + "python.d/" # default configuration directory
495 modules_dir = conf['plugins_dir']
496 except (KeyError, TypeError):
497 modules_dir = main_dir.replace("plugins.d", "python.d")
499 interval = conf['interval']
500 except (KeyError, TypeError):
501 pass # use default interval from NETDATA_UPDATE_EVERY
503 DEBUG_FLAG = conf['debug']
504 except (KeyError, TypeError):
506 for k, v in conf.items():
507 if k in ("plugins_config_dir", "plugins_dir", "interval", "debug"):
512 modules_conf = config_dir + "python.d/"
513 modules_dir = main_dir.replace("plugins.d", "python.d")
515 # directories should end with '/'
516 if modules_dir[-1] != '/':
518 if modules_conf[-1] != '/':
521 # parse passed command line arguments
522 out = parse_cmdline(modules_dir, *sys.argv)
523 modules = out['modules']
524 if out['interval'] is not None:
525 interval = out['interval']
527 # configure environment to run modules
528 sys.path.append(modules_dir + "python_modules") # append path to directory with modules dependencies
531 charts = PythonCharts(interval, modules, modules_dir, modules_conf, disabled)
535 sys.stdout.write("DISABLE")
538 if __name__ == '__main__':