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 is decremented. If there are no more retries, job is stopped.
339 Job is also stopped if it throws an exception.
340 This is also updating job run time chart.
343 t_start = time.time()
344 # check if it is time to execute job update() function
345 if job.timetable['next'] > t_start:
351 since_last = int((t_start - job.timetable['last']) * 1000000)
352 if not job.update(since_last):
354 self._stop(job, "update failed")
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 self.first_run = False
377 Tries to execute update() on every job by using _update_job()
378 This will stay forever and ever and ever forever and ever it'll be the one...
380 self.first_run = True
383 for job in self.jobs:
384 self._update_job(job)
386 next_runs.append(job.timetable['next'])
389 if len(next_runs) == 0:
390 debug("No plugins loaded")
391 sys.stdout.write("DISABLE\n")
393 time.sleep(min(next_runs) - time.time())
396 def read_config(path):
398 Read YAML configuration from specified file
403 with open(path, 'r') as stream:
404 config = yaml.load(stream)
405 except (OSError, IOError):
406 debug(str(path), "is not a valid configuration file")
408 except yaml.YAMLError as e:
409 debug(str(path), "is malformed:", e)
416 Print message on stderr.
420 sys.stderr.write(PROGRAM + ":")
422 sys.stderr.write(" " + str(i))
423 sys.stderr.write("\n")
427 def parse_cmdline(directory, *commands):
429 Parse parameters from command line.
430 :param directory: str
431 :param commands: list of str
438 for cmd in commands[1:]:
441 elif cmd == "debug" or cmd == "all":
443 # redirect stderr to stdout?
444 elif os.path.isfile(directory + cmd + ".chart.py") or os.path.isfile(directory + cmd):
446 mods.append(cmd.replace(".chart.py", ""))
454 debug("started from", commands[0], "with options:", *commands[1:])
455 if len(mods) == 0 and DEBUG_FLAG is False:
458 return {'interval': interval,
462 # if __name__ == '__main__':
467 global PROGRAM, DEBUG_FLAG
468 PROGRAM = sys.argv[0].split('/')[-1].split('.plugin')[0]
469 # parse env variables
470 # https://github.com/firehol/netdata/wiki/External-Plugins#environment-variables
471 main_dir = os.getenv('NETDATA_PLUGINS_DIR',
472 os.path.abspath(__file__).strip("python.d.plugin.py"))
473 config_dir = os.getenv('NETDATA_CONFIG_DIR', "/etc/netdata/")
474 interval = os.getenv('NETDATA_UPDATE_EVERY', None)
476 # read configuration file
478 if config_dir[-1] != '/':
480 configfile = config_dir + "python.d.conf"
482 conf = read_config(configfile)
485 if str(conf['enable']) is False:
486 debug("disabled in configuration file")
487 sys.stdout.write("DISABLE\n")
489 except (KeyError, TypeError):
492 modules_conf = conf['plugins_config_dir']
493 except (KeyError, TypeError):
494 modules_conf = config_dir + "python.d/" # default configuration directory
496 modules_dir = conf['plugins_dir']
497 except (KeyError, TypeError):
498 modules_dir = main_dir.replace("plugins.d", "python.d")
500 interval = conf['interval']
501 except (KeyError, TypeError):
502 pass # use default interval from NETDATA_UPDATE_EVERY
504 DEBUG_FLAG = conf['debug']
505 except (KeyError, TypeError):
507 for k, v in conf.items():
508 if k in ("plugins_config_dir", "plugins_dir", "interval", "debug"):
513 modules_conf = config_dir + "python.d/"
514 modules_dir = main_dir.replace("plugins.d", "python.d")
516 # directories should end with '/'
517 if modules_dir[-1] != '/':
519 if modules_conf[-1] != '/':
522 # parse passed command line arguments
523 out = parse_cmdline(modules_dir, *sys.argv)
524 modules = out['modules']
525 if out['interval'] is not None:
526 interval = out['interval']
528 # configure environment to run modules
529 sys.path.append(modules_dir + "python_modules") # append path to directory with modules dependencies
532 charts = PythonCharts(interval, modules, modules_dir, modules_conf, disabled)
536 sys.stdout.write("DISABLE")
539 if __name__ == '__main__':