2 # -*- coding: utf-8 -*-
4 # Description: netdata python modules supervisor
5 # Author: Pawel Krupa (paulfantom)
12 # -----------------------------------------------------------------------------
13 # globals & environment setup
14 # https://github.com/firehol/netdata/wiki/External-Plugins#environment-variables
15 MODULE_EXTENSION = ".chart.py"
16 BASE_CONFIG = {'update_every': os.getenv('NETDATA_UPDATE_EVERY', 1),
20 MODULES_DIR = os.path.abspath(os.getenv('NETDATA_PLUGINS_DIR',
21 os.path.dirname(__file__)) + "/../python.d") + "/"
22 CONFIG_DIR = os.getenv('NETDATA_CONFIG_DIR', "/etc/netdata/")
23 # directories should end with '/'
24 if CONFIG_DIR[-1] != "/":
26 sys.path.append(MODULES_DIR + "python_modules")
28 PROGRAM = os.path.basename(__file__).replace(".plugin", "")
30 OVERRIDE_UPDATE_EVERY = False
32 # -----------------------------------------------------------------------------
33 # custom, third party and version specific python modules management
37 assert sys.version_info >= (3, 1)
38 import importlib.machinery
40 # change this hack below if we want PY_VERSION to be used in modules
42 # builtins.PY_VERSION = 3
44 msg.info('Using python v3')
45 except (AssertionError, ImportError):
49 # change this hack below if we want PY_VERSION to be used in modules
51 # __builtin__.PY_VERSION = 2
53 msg.info('Using python v2')
55 msg.fatal('Cannot start. No importlib.machinery on python3 or lack of imp on python2')
59 msg.fatal('Cannot find yaml library')
62 class PythonCharts(object):
64 Main class used to control every python module.
69 modules_path='../python.d/',
70 modules_configs='../conf.d/',
71 modules_disabled=None):
74 :param modules_path: str
75 :param modules_configs: str
76 :param modules_disabled: list
81 if modules_disabled is None:
85 # set configuration directory
86 self.configs = modules_configs
89 loaded_modules = self._load_modules(modules_path, modules, modules_disabled)
91 # load configuration files
92 configured_modules = self._load_configs(loaded_modules)
94 # good economy and prosperity:
95 self.jobs = self._create_jobs(configured_modules) # type: list
97 # enable timetable override like `python.d.plugin mysql debug 1`
98 if DEBUG_FLAG and OVERRIDE_UPDATE_EVERY:
100 job.create_timetable(BASE_CONFIG['update_every'])
103 def _import_module(path, name=None):
105 Try to import module using only its path.
112 name = path.split('/')[-1]
113 if name[-len(MODULE_EXTENSION):] != MODULE_EXTENSION:
115 name = name[:-len(MODULE_EXTENSION)]
118 return importlib.machinery.SourceFileLoader(name, path).load_module()
120 return imp.load_source(name, path)
121 except Exception as e:
122 msg.error("Problem loading", name, str(e))
125 def _load_modules(self, path, modules, disabled):
127 Load modules from 'modules' list or dynamically every file from 'path' (only .chart.py files)
130 :param disabled: list
134 # check if plugin directory exists
135 if not os.path.isdir(path):
136 msg.fatal("cannot find charts directory ", path)
144 mod = self._import_module(path + m + MODULE_EXTENSION)
147 else: # exit if plugin is not found
148 msg.fatal('no modules found.')
150 # scan directory specified in path and load all modules from there
151 names = os.listdir(path)
153 if mod.replace(MODULE_EXTENSION, "") in disabled:
154 msg.error(mod + ": disabled module ", mod.replace(MODULE_EXTENSION, ""))
156 m = self._import_module(path + mod)
158 msg.debug(mod + ": loading module '" + path + mod + "'")
162 def _load_configs(self, modules):
164 Append configuration in list named `config` to every module.
165 For multi-job modules `config` list is created in _parse_config,
166 otherwise it is created here based on BASE_CONFIG prototype with None as identifier.
171 configfile = self.configs + mod.__name__ + ".conf"
172 if os.path.isfile(configfile):
173 msg.debug(mod.__name__ + ": loading module configuration: '" + configfile + "'")
175 if not hasattr(mod, 'config'):
179 self._parse_config(mod, read_config(configfile)))
180 except Exception as e:
181 msg.error(mod.__name__ + ": cannot parse configuration file '" + configfile + "':", str(e))
183 msg.error(mod.__name__ + ": configuration file '" + configfile + "' not found. Using defaults.")
184 # set config if not found
185 if not hasattr(mod, 'config'):
186 msg.debug(mod.__name__ + ": setting configuration for only one job")
187 mod.config = {None: {}}
188 for var in BASE_CONFIG:
190 mod.config[None][var] = getattr(mod, var)
191 except AttributeError:
192 mod.config[None][var] = BASE_CONFIG[var]
196 def _parse_config(module, config):
198 Parse configuration file or extract configuration from module file.
199 Example of returned dictionary:
205 :param module: object
213 msg.debug(module.__name__ + ": reading configuration")
214 for key in BASE_CONFIG:
216 # get defaults from module config
217 defaults[key] = int(config.pop(key))
218 except (KeyError, ValueError):
220 # get defaults from module source code
221 defaults[key] = getattr(module, key)
222 except (KeyError, ValueError, AttributeError):
223 # if above failed, get defaults from global dict
224 defaults[key] = BASE_CONFIG[key]
226 # check if there are dict in config dict
229 if type(config[name]) is dict:
233 # assign variables needed by supervisor to every job configuration
237 if key not in config[name]:
238 config[name][key] = defaults[key]
239 # if only one job is needed, values doesn't have to be in dict (in YAML)
241 config = {None: config.copy()}
242 config[None].update(defaults)
244 # return dictionary of jobs where every job has BASE_CONFIG variables
248 def _create_jobs(modules):
250 Create jobs based on module.config dictionary and module.Service class definition.
255 for module in modules:
256 for name in module.config:
258 conf = module.config[name]
260 job = module.Service(configuration=conf, name=name)
261 except Exception as e:
262 msg.error(module.__name__ +
263 ("/" + str(name) if name is not None else "") +
264 ": cannot start job: '" +
268 # set chart_name (needed to plot run time graphs)
269 job.chart_name = module.__name__
271 job.chart_name += "_" + name
273 msg.debug(module.__name__ + ("/" + str(name) if name is not None else "") + ": job added")
275 return [j for j in jobs if j is not None]
277 def _stop(self, job, reason=None):
279 Stop specified job and remove it from self.jobs list
280 Also notifies user about job failure if DEBUG_FLAG is set
284 prefix = job.__module__
285 if job.name is not None:
286 prefix += "/" + job.name
289 self.jobs.remove(job)
292 elif reason[:3] == "no ":
294 "does not seem to have " +
296 "() function. Disabling it.")
297 elif reason[:7] == "failed ":
300 "() function reports failure.")
301 elif reason[:13] == "configuration":
303 "configuration file '" +
306 ".conf' not found. Using defaults.")
307 elif reason[:11] == "misbehaving":
308 msg.error(prefix + "is " + reason)
312 Tries to execute check() on every job.
313 This cannot fail thus it is catching every exception
314 If job.check() fails job is stopped
318 while i < len(self.jobs):
320 if job.name in overridden:
322 msg.error(job.name + " already exists")
325 self._stop(job, "failed check")
327 msg.debug(job.chart_name, ": check succeeded")
330 if job.override_name is not None:
331 job.name = job.override_name
332 msg.debug(job.chart_name + " changing chart name to: " + job.__module__ + job.name)
333 job.chart_name = job.__module__ + job.name
334 overridden.append(job.name)
337 except AttributeError:
338 self._stop(job, "no check")
339 except (UnboundLocalError, Exception) as e:
340 self._stop(job, "misbehaving. Reason:" + str(e))
344 Tries to execute create() on every job.
345 This cannot fail thus it is catching every exception.
346 If job.create() fails job is stopped.
347 This is also creating job run time chart.
350 while i < len(self.jobs):
354 self._stop(job, "failed create")
356 chart = job.chart_name
358 "CHART netdata.plugin_pythond_" +
360 " '' 'Execution time for " +
362 " plugin' 'milliseconds / run' python.d netdata.plugin_python area 145000 " +
363 str(job.timetable['freq']) +
365 sys.stdout.write("DIMENSION run_time 'run time' absolute 1 1\n\n")
366 msg.debug("created charts for", job.chart_name)
369 except AttributeError:
370 self._stop(job, "no create")
371 except (UnboundLocalError, Exception) as e:
372 self._stop(job, "misbehaving. Reason: " + str(e))
376 Creates and supervises every job thread.
377 This will stay forever and ever and ever forever and ever it'll be the one...
379 for job in self.jobs:
383 if threading.active_count() <= 1:
384 msg.fatal("no more jobs")
388 def read_config(path):
390 Read YAML configuration from specified file
395 with open(path, 'r') as stream:
396 config = yaml.load(stream)
397 except (OSError, IOError):
398 msg.error(str(path), "is not a valid configuration file")
400 except yaml.YAMLError as e:
401 msg.error(str(path), "is malformed:", e)
406 def parse_cmdline(directory, *commands):
408 Parse parameters from command line.
409 :param directory: str
410 :param commands: list of str
414 global OVERRIDE_UPDATE_EVERY
417 changed_update = False
419 for cmd in commands[1:]:
422 elif cmd == "debug" or cmd == "all":
424 # redirect stderr to stdout?
425 elif os.path.isfile(directory + cmd + ".chart.py") or os.path.isfile(directory + cmd):
427 mods.append(cmd.replace(".chart.py", ""))
430 BASE_CONFIG['update_every'] = int(cmd)
431 changed_update = True
434 if changed_update and DEBUG_FLAG:
435 OVERRIDE_UPDATE_EVERY = True
436 msg.debug(PROGRAM, "overriding update interval to", str(BASE_CONFIG['update_every']))
438 msg.debug("started from", commands[0], "with options:", *commands[1:])
443 # if __name__ == '__main__':
448 global DEBUG_FLAG, BASE_CONFIG
450 # read configuration file
452 configfile = CONFIG_DIR + "python.d.conf"
453 msg.PROGRAM = PROGRAM
454 msg.info("reading configuration file:", configfile)
456 conf = read_config(configfile)
459 # exit the whole plugin when 'enabled: no' is set in 'python.d.conf'
460 if conf['enabled'] is False:
461 msg.fatal('disabled in configuration file.\n')
462 except (KeyError, TypeError):
465 for param in BASE_CONFIG:
466 BASE_CONFIG[param] = conf[param]
467 except (KeyError, TypeError):
468 pass # use default update_every from NETDATA_UPDATE_EVERY
470 DEBUG_FLAG = conf['debug']
471 except (KeyError, TypeError):
473 for k, v in conf.items():
474 if k in ("update_every", "debug", "enabled"):
479 # parse passed command line arguments
480 modules = parse_cmdline(MODULES_DIR, *sys.argv)
481 msg.DEBUG_FLAG = DEBUG_FLAG
482 msg.info("MODULES_DIR='" + MODULES_DIR +
483 "', CONFIG_DIR='" + CONFIG_DIR +
484 "', UPDATE_EVERY=" + str(BASE_CONFIG['update_every']) +
485 ", ONLY_MODULES=" + str(modules))
488 charts = PythonCharts(modules, MODULES_DIR, CONFIG_DIR + "python.d/", disabled)
492 msg.fatal("finished")
495 if __name__ == '__main__':