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.
157 configfile = self.configs + mod.__name__ + ".conf"
158 if os.path.isfile(configfile):
159 debug("loading chart options: '" + configfile + "'")
163 self._parse_config(mod, read_config(configfile)))
164 except Exception as e:
165 debug("something went wrong while loading configuration", e)
168 ": configuration file '" +
170 "' not found. Using defaults.")
171 # set config if not found
172 if not hasattr(mod, 'config'):
173 mod.config = {None: {}}
174 for var in BASE_CONFIG:
176 mod.config[None][var] = getattr(mod, var)
177 except AttributeError:
178 mod.config[None][var] = BASE_CONFIG[var]
182 def _parse_config(module, config):
184 Parse configuration file or extract configuration from module file.
185 Example of returned dictionary:
191 :param module: object
198 for key in BASE_CONFIG:
200 # get defaults from module config
201 defaults[key] = int(config.pop(key))
202 except (KeyError, ValueError):
204 # get defaults from module source code
205 defaults[key] = getattr(module, key)
206 except (KeyError, ValueError):
207 # if above failed, get defaults from global dict
208 defaults[key] = BASE_CONFIG[key]
210 # check if there are dict in config dict
213 if type(config[name]) is dict:
217 # assign variables needed by supervisor to every job configuration
221 if key not in config[name]:
222 config[name][key] = defaults[key]
223 # if only one job is needed, values doesn't have to be in dict (in YAML)
225 config = {None: config.copy()}
226 config[None].update(defaults)
228 # return dictionary of jobs where every job has BASE_CONFIG variables
232 def _create_jobs(modules):
234 Create jobs based on module.config dictionary and module.Service class definition.
239 for module in modules:
240 for name in module.config:
242 conf = module.config[name]
244 job = module.Service(configuration=conf, name=name)
245 except Exception as e:
246 debug(module.__name__ +
247 ": Couldn't start job named " +
253 # set execution_name (needed to plot run time graphs)
254 job.execution_name = module.__name__
256 job.execution_name += "_" + name
259 return [j for j in jobs if j is not None]
261 def _stop(self, job, reason=None):
263 Stop specified job and remove it from self.jobs list
264 Also notifies user about job failure if DEBUG_FLAG is set
268 self.jobs.remove(job)
271 elif reason[:3] == "no ":
274 "' does not seem to have " +
276 "() function. Disabling it.")
277 elif reason[:7] == "failed ":
279 job.execution_name + "' " +
281 "() function reports failure.")
282 elif reason[:13] == "configuration":
283 debug(job.execution_name,
284 "configuration file '" +
287 ".conf' not found. Using defaults.")
288 elif reason[:11] == "misbehaving":
289 debug(job.execution_name, "is " + reason)
293 Tries to execute check() on every job.
294 This cannot fail thus it is catching every exception
295 If job.check() fails job is stopped
297 for job in self.jobs:
300 self._stop(job, "failed check")
301 except AttributeError:
302 self._stop(job, "no check")
303 except (UnboundLocalError, Exception) as e:
304 self._stop(job, "misbehaving. Reason: " + str(e))
308 Tries to execute create() on every job.
309 This cannot fail thus it is catching every exception.
310 If job.create() fails job is stopped.
311 This is also creating job run time chart.
313 for job in self.jobs:
316 self._stop(job, "failed create")
318 chart = job.execution_name
320 "CHART netdata.plugin_pythond_" +
322 " '' 'Execution time for " +
324 " plugin' 'milliseconds / run' python.d netdata.plugin_python area 145000 " +
325 str(job.timetable['freq']) +
327 sys.stdout.write("DIMENSION run_time 'run time' absolute 1 1\n\n")
329 except AttributeError:
330 self._stop(job, "no create")
331 except (UnboundLocalError, Exception) as e:
332 self._stop(job, "misbehaving. Reason: " + str(e))
334 def _update_job(self, job):
336 Tries to execute update() on specified job.
337 This cannot fail thus it is catching every exception.
338 If job.update() returns False, number of retries_left is decremented.
339 If there are no more retries, job is stopped.
340 Job is also stopped if it throws an exception.
341 This is also updating job run time chart.
344 t_start = time.time()
345 # check if it is time to execute job update() function
346 if job.timetable['next'] > t_start:
352 since_last = int((t_start - job.timetable['last']) * 1000000)
353 if not job.update(since_last):
354 if job.retries_left <= 0:
355 self._stop(job, "update failed")
356 job.retries_left -= 1
357 job.timetable['next'] += job.timetable['freq']
359 except AttributeError:
360 self._stop(job, "no update")
362 except (UnboundLocalError, Exception) as e:
363 self._stop(job, "misbehaving. Reason: " + str(e))
366 job.timetable['next'] = t_end - (t_end % job.timetable['freq']) + job.timetable['freq']
367 # draw performance graph
368 sys.stdout.write("BEGIN netdata.plugin_pythond_" + job.execution_name + " " + str(since_last) + '\n')
369 sys.stdout.write("SET run_time = " + str(int((t_end - t_start) * 1000)) + '\n')
370 sys.stdout.write("END\n")
372 job.timetable['last'] = t_start
373 job.retries_left = job.retries
374 self.first_run = False
378 Tries to execute update() on every job by using _update_job()
379 This will stay forever and ever and ever forever and ever it'll be the one...
381 self.first_run = True
384 for job in self.jobs:
385 self._update_job(job)
387 next_runs.append(job.timetable['next'])
390 if len(next_runs) == 0:
391 debug("No plugins loaded")
392 sys.stdout.write("DISABLE\n")
394 time.sleep(min(next_runs) - time.time())
397 def read_config(path):
399 Read YAML configuration from specified file
404 with open(path, 'r') as stream:
405 config = yaml.load(stream)
406 except (OSError, IOError):
407 debug(str(path), "is not a valid configuration file")
409 except yaml.YAMLError as e:
410 debug(str(path), "is malformed:", e)
417 Print message on stderr.
421 sys.stderr.write(PROGRAM + ":")
423 sys.stderr.write(" " + str(i))
424 sys.stderr.write("\n")
428 def parse_cmdline(directory, *commands):
430 Parse parameters from command line.
431 :param directory: str
432 :param commands: list of str
439 for cmd in commands[1:]:
442 elif cmd == "debug" or cmd == "all":
444 # redirect stderr to stdout?
445 elif os.path.isfile(directory + cmd + ".chart.py") or os.path.isfile(directory + cmd):
447 mods.append(cmd.replace(".chart.py", ""))
455 debug("started from", commands[0], "with options:", *commands[1:])
456 if len(mods) == 0 and DEBUG_FLAG is False:
459 return {'interval': interval,
463 # if __name__ == '__main__':
468 global PROGRAM, DEBUG_FLAG
469 PROGRAM = sys.argv[0].split('/')[-1].split('.plugin')[0]
470 # parse env variables
471 # https://github.com/firehol/netdata/wiki/External-Plugins#environment-variables
472 main_dir = os.getenv('NETDATA_PLUGINS_DIR',
473 os.path.abspath(__file__).strip("python.d.plugin.py"))
474 config_dir = os.getenv('NETDATA_CONFIG_DIR', "/etc/netdata/")
475 interval = os.getenv('NETDATA_UPDATE_EVERY', None)
477 # read configuration file
479 if config_dir[-1] != '/':
481 configfile = config_dir + "python.d.conf"
483 conf = read_config(configfile)
486 if str(conf['enable']) is False:
487 debug("disabled in configuration file")
488 sys.stdout.write("DISABLE\n")
490 except (KeyError, TypeError):
493 modules_conf = conf['plugins_config_dir']
494 except (KeyError, TypeError):
495 modules_conf = config_dir + "python.d/" # default configuration directory
497 modules_dir = conf['plugins_dir']
498 except (KeyError, TypeError):
499 modules_dir = main_dir.replace("plugins.d", "python.d")
501 interval = conf['interval']
502 except (KeyError, TypeError):
503 pass # use default interval from NETDATA_UPDATE_EVERY
505 DEBUG_FLAG = conf['debug']
506 except (KeyError, TypeError):
508 for k, v in conf.items():
509 if k in ("plugins_config_dir", "plugins_dir", "interval", "debug"):
514 modules_conf = config_dir + "python.d/"
515 modules_dir = main_dir.replace("plugins.d", "python.d")
517 # directories should end with '/'
518 if modules_dir[-1] != '/':
520 if modules_conf[-1] != '/':
523 # parse passed command line arguments
524 out = parse_cmdline(modules_dir, *sys.argv)
525 modules = out['modules']
526 if out['interval'] is not None:
527 interval = out['interval']
529 # configure environment to run modules
530 sys.path.append(modules_dir + "python_modules") # append path to directory with modules dependencies
533 charts = PythonCharts(interval, modules, modules_dir, modules_conf, disabled)
537 sys.stdout.write("DISABLE")
540 if __name__ == '__main__':