#!/usr/bin/env python3 import os import sys import time try: assert sys.version_info >= (3, 1) import importlib.machinery # change this hack below if we want PY_VERSION to be used in modules # import builtins # builtins.PY_VERSION = 3 PY_VERSION = 3 sys.stderr.write('python.d.plugin: Using python 3\n') except (AssertionError, ImportError): try: import imp # change this hack below if we want PY_VERSION to be used in modules # import __builtin__ # __builtin__.PY_VERSION = 2 PY_VERSION = 2 sys.stderr.write('python.d.plugin: Using python 2\n') except (AssertionError, ImportError): sys.stderr.write('python.d.plugin: Not supported python version. Needed python >= 3.1\n') sys.stdout.write('DISABLE\n') sys.exit(1) try: import yaml except ImportError: sys.stderr.write('python.d.plugin: Cannot find yaml library\n') sys.stdout.write('DISABLE\n') sys.exit(1) DEBUG_FLAG = False PROGRAM = "python.d.plugin" MODULE_EXTENSION = ".chart.py" BASE_CONFIG = {'update_every': 10, 'priority': 12345, 'retries': 0} class PythonCharts(object): def __init__(self, interval=None, modules=None, modules_path='../python.d/', modules_configs='../conf.d/', modules_disabled=None): if modules is None: modules = [] if modules_disabled is None: modules_disabled = [] self.first_run = True # set configuration directory self.configs = modules_configs # load modules loaded_modules = self._load_modules(modules_path, modules, modules_disabled) # load configuration files configured_modules = self._load_configs(loaded_modules) # good economy and prosperity: self.jobs = self._create_jobs(configured_modules) if DEBUG_FLAG and interval is not None: for job in self.jobs: job.create_timetable(interval) @staticmethod def _import_module(path, name=None): # try to import module using only its path if name is None: name = path.split('/')[-1] if name[-len(MODULE_EXTENSION):] != MODULE_EXTENSION: return None name = name[:-len(MODULE_EXTENSION)] try: if PY_VERSION == 3: return importlib.machinery.SourceFileLoader(name, path).load_module() else: return imp.load_source(name, path) # return importlib.import_module(path, name) except Exception as e: debug(str(e)) return None def _load_modules(self, path, modules, disabled): # check if plugin directory exists if not os.path.isdir(path): debug("cannot find charts directory ", path) sys.stdout.write("DISABLE\n") sys.exit(1) # load modules loaded = [] if len(modules) > 0: for m in modules: if m in disabled: continue mod = self._import_module(path + m + MODULE_EXTENSION) if mod is not None: loaded.append(mod) else: # exit if plugin is not found sys.stdout.write("DISABLE") sys.stdout.flush() sys.exit(1) else: # scan directory specified in path and load all modules from there names = os.listdir(path) for mod in names: if mod.strip(MODULE_EXTENSION) in disabled: debug("disabling:", mod.strip(MODULE_EXTENSION)) continue m = self._import_module(path + mod) if m is not None: debug("loading chart: '" + path + mod + "'") loaded.append(m) return loaded def _load_configs(self, modules): # function loads configuration files to modules for mod in modules: configfile = self.configs + mod.__name__ + ".conf" if os.path.isfile(configfile): debug("loading chart options: '" + configfile + "'") try: setattr(mod, 'config', self._parse_config(mod, read_config(configfile))) except Exception as e: debug("something went wrong while loading configuration", e) else: debug(mod.__name__ + ": configuration file '" + configfile + "' not found. Using defaults.") # set config if not found if not hasattr(mod, 'config'): mod.config = {None: {}} for var in BASE_CONFIG: try: mod.config[None][var] = getattr(mod, var) except AttributeError: mod.config[None][var] = BASE_CONFIG[var] return modules @staticmethod def _parse_config(module, config): # get default values defaults = {} for key in BASE_CONFIG: try: # get defaults from module config defaults[key] = int(config.pop(key)) except (KeyError, ValueError): try: # get defaults from module source code defaults[key] = getattr(module, key) except (KeyError, ValueError): # if above failed, get defaults from global dict defaults[key] = BASE_CONFIG[key] # check if there are dict in config dict many_jobs = False for name in config: if type(config[name]) is dict: many_jobs = True break # assign variables needed by supervisor to every job configuration if many_jobs: for name in config: for key in defaults: if key not in config[name]: config[name][key] = defaults[key] # if only one job is needed, values doesn't have to be in dict (in YAML) else: config = {None: config.copy()} config[None].update(defaults) # return dictionary of jobs where every job has BASE_CONFIG variables return config @staticmethod def _create_jobs(modules): # module store a definition of Service class # module store configuration in module.config # configs are list of dicts or a dict # one dict is one service # iterate over list of modules and inside one loop iterate over configs jobs = [] for module in modules: for name in module.config: # register a new job conf = module.config[name] try: job = module.Service(configuration=conf, name=name) except Exception as e: debug(module.__name__ + ": Couldn't start job named " + str(name) + ": " + str(e)) return None else: # set execution_name (needed to plot run time graphs) job.execution_name = module.__name__ if name is not None: job.execution_name += "_" + name jobs.append(job) return [j for j in jobs if j is not None] def _stop(self, job, reason=None): # modifies self.jobs self.jobs.remove(job) if reason is None: return elif reason[:3] == "no ": debug("chart '" + job.execution_name, "' does not seem to have " + reason[3:] + "() function. Disabling it.") elif reason[:7] == "failed ": debug("chart '" + job.execution_name + "' " + reason[7:] + "() function reports failure.") elif reason[:13] == "configuration": debug(job.execution_name, "configuration file '" + self.configs + job.execution_name + ".conf' not found. Using defaults.") elif reason[:11] == "misbehaving": debug(job.execution_name, "is " + reason) def check(self): # try to execute check() on every job for job in self.jobs: try: if not job.check(): self._stop(job, "failed check") except AttributeError: self._stop(job, "no check") except (UnboundLocalError, Exception) as e: self._stop(job, "misbehaving. Reason: " + str(e)) def create(self): # try to execute create() on every job for job in self.jobs: try: if not job.create(): self._stop(job, "failed create") else: chart = job.execution_name sys.stdout.write( "CHART netdata.plugin_pythond_" + chart + " '' 'Execution time for " + chart + " plugin' 'milliseconds / run' python.d netdata.plugin_python area 145000 " + str(job.timetable['freq']) + '\n') sys.stdout.write("DIMENSION run_time 'run time' absolute 1 1\n\n") sys.stdout.flush() except AttributeError: self._stop(job, "no create") except (UnboundLocalError, Exception) as e: self._stop(job, "misbehaving. Reason: " + str(e)) def _update_job(self, job): # try to execute update() on every job and draw run time graph t_start = time.time() # check if it is time to execute job update() function if job.timetable['next'] > t_start: return try: if self.first_run: since_last = 0 else: since_last = int((t_start - job.timetable['last']) * 1000000) if not job.update(since_last): self._stop(job, "update failed") return except AttributeError: self._stop(job, "no update") return except (UnboundLocalError, Exception) as e: self._stop(job, "misbehaving. Reason: " + str(e)) return t_end = time.time() job.timetable['next'] = t_end - (t_end % job.timetable['freq']) + job.timetable['freq'] # draw performance graph sys.stdout.write("BEGIN netdata.plugin_pythond_" + job.execution_name + " " + str(since_last) + '\n') sys.stdout.write("SET run_time = " + str(int((t_end - t_start) * 1000)) + '\n') sys.stdout.write("END\n") sys.stdout.flush() job.timetable['last'] = t_start self.first_run = False def update(self): # run updates (this will stay forever and ever and ever forever and ever it'll be the one...) self.first_run = True while True: next_runs = [] for job in self.jobs: self._update_job(job) try: next_runs.append(job.timetable['next']) except KeyError: pass if len(next_runs) == 0: debug("No plugins loaded") sys.stdout.write("DISABLE\n") sys.exit(1) time.sleep(min(next_runs) - time.time()) def read_config(path): try: with open(path, 'r') as stream: config = yaml.load(stream) except (OSError, IOError): debug(str(path), "is not a valid configuration file") return None except yaml.YAMLError as e: debug(str(path), "is malformed:", e) return None return config def debug(*args): if not DEBUG_FLAG: return sys.stderr.write(PROGRAM + ":") for i in args: sys.stderr.write(" " + str(i)) sys.stderr.write("\n") sys.stderr.flush() def parse_cmdline(directory, *commands): global DEBUG_FLAG interval = None mods = [] for cmd in commands[1:]: if cmd == "check": pass elif cmd == "debug" or cmd == "all": DEBUG_FLAG = True # redirect stderr to stdout? elif os.path.isfile(directory + cmd + ".chart.py") or os.path.isfile(directory + cmd): DEBUG_FLAG = True mods.append(cmd.replace(".chart.py", "")) else: DEBUG_FLAG = False try: interval = int(cmd) except ValueError: pass debug("started from", commands[0], "with options:", *commands[1:]) if len(mods) == 0 and DEBUG_FLAG is False: interval = None return {'interval': interval, 'modules': mods} # if __name__ == '__main__': def run(): global PROGRAM, DEBUG_FLAG PROGRAM = sys.argv[0].split('/')[-1].split('.plugin')[0] # parse env variables # https://github.com/firehol/netdata/wiki/External-Plugins#environment-variables main_dir = os.getenv('NETDATA_PLUGINS_DIR', os.path.abspath(__file__).strip("python.d.plugin.py")) config_dir = os.getenv('NETDATA_CONFIG_DIR', "/etc/netdata/") interval = os.getenv('NETDATA_UPDATE_EVERY', None) # read configuration file disabled = [] if config_dir[-1] != '/': config_dir += '/' configfile = config_dir + "python.d.conf" conf = read_config(configfile) if conf is not None: try: if str(conf['enable']) is False: debug("disabled in configuration file") sys.stdout.write("DISABLE\n") sys.exit(1) except (KeyError, TypeError): pass try: modules_conf = conf['plugins_config_dir'] except (KeyError, TypeError): modules_conf = config_dir + "python.d/" # default configuration directory try: modules_dir = conf['plugins_dir'] except (KeyError, TypeError): modules_dir = main_dir.replace("plugins.d", "python.d") try: interval = conf['interval'] except (KeyError, TypeError): pass # use default interval from NETDATA_UPDATE_EVERY try: DEBUG_FLAG = conf['debug'] except (KeyError, TypeError): pass for k, v in conf.items(): if k in ("plugins_config_dir", "plugins_dir", "interval", "debug"): continue if v is False: disabled.append(k) else: modules_conf = config_dir + "python.d/" modules_dir = main_dir.replace("plugins.d", "python.d") # directories should end with '/' if modules_dir[-1] != '/': modules_dir += "/" if modules_conf[-1] != '/': modules_conf += "/" # parse passed command line arguments out = parse_cmdline(modules_dir, *sys.argv) modules = out['modules'] if out['interval'] is not None: interval = out['interval'] # configure environment to run modules sys.path.append(modules_dir + "python_modules") # append path to directory with modules dependencies # run plugins charts = PythonCharts(interval, modules, modules_dir, modules_conf, disabled) charts.check() charts.create() charts.update() sys.stdout.write("DISABLE") if __name__ == '__main__': run()