7 assert sys.version_info >= (3,1)
8 import importlib.machinery
10 sys.stderr.write('python.d.plugin: Not supported python version. Needed python >= 3.1\n')
11 sys.stdout.write('DISABLE\n')
16 sys.stderr.write('python.d.plugin: Cannot find yaml library\n')
17 sys.stdout.write('DISABLE\n')
20 MODULE_EXTENSION = ".chart.py"
21 BASE_CONFIG = {'update_every' : 10,
26 class PythonCharts(object):
31 modules_path='../python.d/',
32 modules_configs='../conf.d/',
35 # set configuration directory
36 self.configs = modules_configs
39 loaded_modules = self._load_modules(modules_path,modules, modules_disabled)
41 # load configuration files
42 configured_modules = self._load_configs(loaded_modules)
44 # good economy and prosperity:
45 self.jobs = self._create_jobs(configured_modules)
46 if DEBUG_FLAG and interval is not None:
48 job._create_timetable(interval)
51 def _create_jobs(self,modules):
52 # module store a definition of Service class
53 # module store configuration in module.config
54 # configs are list of dicts or a dict
55 # one dict is one service
56 # iterate over list of modules and inside one loop iterate over configs
58 for module in modules:
59 for name in module.config:
61 conf = module.config[name]
63 job = module.Service(configuration=conf, name=name)
64 except Exception as e:
65 debug(module.__name__ +
66 ": Couldn't start job named " +
72 # set execution_name (needed to plot run time graphs)
73 job.execution_name = module.__name__
75 job.execution_name += "_" + name
78 return [j for j in jobs if j is not None]
80 def _import_module(self, path, name=None):
81 # try to import module using only its path
83 name = path.split('/')[-1]
84 if name[-len(MODULE_EXTENSION):] != MODULE_EXTENSION:
86 name = name[:-len(MODULE_EXTENSION)]
88 return importlib.machinery.SourceFileLoader(name, path).load_module()
89 except Exception as e:
93 def _load_modules(self, path, modules, disabled):
94 # check if plugin directory exists
95 if not os.path.isdir(path):
96 debug("cannot find charts directory ", path)
97 sys.stdout.write("DISABLE\n")
106 mod = self._import_module(path + m + MODULE_EXTENSION)
110 # scan directory specified in path and load all modules from there
111 names = os.listdir(path)
113 if mod.strip(MODULE_EXTENSION) in disabled:
114 debug("disabling:",mod.strip(MODULE_EXTENSION))
116 m = self._import_module(path + mod)
118 debug("loading chart: '" + path + mod + "'")
122 def _load_configs(self,modules):
123 # function loads configuration files to modules
125 configfile = self.configs + mod.__name__ + ".conf"
126 if os.path.isfile(configfile):
127 debug("loading chart options: '" + configfile + "'")
131 self._parse_config(mod,read_config(configfile)))
132 except Exception as e:
133 debug("something went wrong while loading configuration",e)
136 ": configuration file '" +
138 "' not found. Using defaults.")
139 # set config if not found
140 if not hasattr(mod, 'config'):
141 mod.config = {None:{}}
142 for var in BASE_CONFIG:
144 mod.config[None][var] = getattr(var, mod)
146 mod.config[None][var] = BASE_CONFIG[var]
149 def _parse_config(self,module,config):
152 for key in BASE_CONFIG:
154 # get defaults from module config
155 defaults[key] = int(config.pop(key))
156 except (KeyError,ValueError):
158 # get defaults from module source code
159 defaults[key] = getattr(module, key)
160 except (KeyError,ValueError):
161 # if above failed, get defaults from global dict
162 defaults[key] = BASE_CONFIG[key]
164 # check if there are dict in config dict
167 if type(config[name]) is dict:
171 # assign variables needed by supervisor to every job configuration
175 if key not in config[name]:
176 config[name][key] = defaults[key]
177 # if only one job is needed, values doesn't have to be in dict (in YAML)
179 config = {None: config.copy()}
180 config[None].update(defaults)
182 # return dictionary of jobs where every job has BASE_CONFIG variables
185 def _stop(self, job, reason=None):
187 self.jobs.remove(job)
190 elif reason[:3] == "no ":
193 "' does not seem to have " +
195 "() function. Disabling it.")
196 elif reason[:7] == "failed ":
198 job.execution_name + "' " +
200 "() function reports failure.")
201 elif reason[:13] == "configuration":
202 debug(job.execution_name,
203 "configuration file '" +
206 ".conf' not found. Using defaults.")
207 elif reason[:11] == "misbehaving":
208 debug(job.execution_name, "is "+reason)
211 # try to execute check() on every job
212 for job in self.jobs:
215 self._stop(job, "failed check")
216 except AttributeError:
217 self._stop(job, "no check")
218 except (UnboundLocalError, Exception) as e:
219 self._stop(job, "misbehaving. Reason: " + str(e))
222 # try to execute create() on every job
223 for job in self.jobs:
226 self._stop(job, "failed create")
228 chart = job.execution_name
230 "CHART netdata.plugin_pythond_" +
232 " '' 'Execution time for " +
234 " plugin' 'milliseconds / run' python.d netdata.plugin_python area 145000 " +
235 str(job.timetable['freq']) +
237 sys.stdout.write("DIMENSION run_time 'run time' absolute 1 1\n\n")
239 except AttributeError:
240 self._stop(job, "no create")
241 except (UnboundLocalError, Exception) as e:
242 self._stop(job, "misbehaving. Reason: " + str(e))
244 def _update_job(self, job):
245 # try to execute update() on every job and draw run time graph
246 t_start = time.time()
247 # check if it is time to execute job update() function
248 if job.timetable['next'] > t_start:
254 since_last = int((t_start - job.timetable['last']) * 1000000)
255 if not job.update(since_last):
256 self._stop(job, "update failed")
258 except AttributeError:
259 self._stop(job, "no update")
261 except (UnboundLocalError, Exception) as e:
262 self._stop(job, "misbehaving. Reason: " + str(e))
265 job.timetable['next'] = t_end - (t_end % job.timetable['freq']) + job.timetable['freq']
266 # draw performance graph
270 dt = int((t_end - job.timetable['last']) * 1000000)
271 sys.stdout.write("BEGIN netdata.plugin_pythond_"+job.execution_name+" "+str(since_last)+'\n')
272 sys.stdout.write("SET run_time = " + str(int((t_end - t_start) * 1000)) + '\n')
273 sys.stdout.write("END\n")
275 job.timetable['last'] = t_start
276 self.first_run = False
279 # run updates (this will stay forever and ever and ever forever and ever it'll be the one...)
280 self.first_run = True
282 t_begin = time.time()
284 for job in self.jobs:
285 self._update_job(job)
287 next_runs.append(job.timetable['next'])
290 if len(next_runs) == 0:
291 debug("No plugins loaded")
292 sys.stdout.write("DISABLE\n")
294 time.sleep(min(next_runs) - time.time())
297 def read_config(path):
299 with open(path, 'r') as stream:
300 config = yaml.load(stream)
301 except IsADirectoryError:
302 debug(str(path), "is a directory")
304 except yaml.YAMLError as e:
305 debug(str(path), "is malformed:", e)
313 sys.stderr.write(PROGRAM + ":")
315 sys.stderr.write(" " + str(i))
316 sys.stderr.write("\n")
320 def parse_cmdline(directory, *commands):
321 # TODO number -> interval
327 for cmd in commands[1:]:
330 elif cmd == "debug" or cmd == "all":
332 # redirect stderr to stdout?
333 elif os.path.isfile(directory + cmd + ".chart.py") or os.path.isfile(directory + cmd):
335 mods.append(cmd.replace(".chart.py", ""))
342 debug("started from", commands[0], "with options:", *commands[1:])
343 if len(mods) == 0 and DEBUG_FLAG is False:
346 return {'interval': interval,
350 # if __name__ == '__main__':
352 global DEBUG_FLAG, PROGRAM
354 PROGRAM = sys.argv[0].split('/')[-1].split('.plugin')[0]
355 # parse env variables
356 # https://github.com/firehol/netdata/wiki/External-Plugins#environment-variables
357 main_dir = os.getenv('NETDATA_PLUGINS_DIR',
358 os.path.abspath(__file__).strip("python.d.plugin.py"))
359 config_dir = os.getenv('NETDATA_CONFIG_DIR', "/etc/netdata/")
360 interval = os.getenv('NETDATA_UPDATE_EVERY', None)
362 # read configuration file
364 if config_dir[-1] != '/':
366 configfile = config_dir + "python.d.conf"
370 conf = read_config(configfile)
373 if str(conf['enable']) is False:
374 debug("disabled in configuration file")
375 sys.stdout.write("DISABLE\n")
377 except (KeyError, TypeError):
380 modules_conf = conf['plugins_config_dir']
382 modules_conf = config_dir + "python.d/" # default configuration directory
384 modules_dir = conf['plugins_dir']
386 modules_dir = main_dir.replace("plugins.d", "python.d")
388 interval = conf['interval']
390 pass # use default interval from NETDATA_UPDATE_EVERY
392 DEBUG_FLAG = conf['debug']
395 for k, v in conf.items():
396 if k in ("plugins_config_dir", "plugins_dir", "interval", "debug"):
400 except FileNotFoundError:
401 modules_conf = config_dir
402 modules_dir = main_dir.replace("plugins.d", "python.d")
404 # directories should end with '/'
405 if modules_dir[-1] != '/':
407 if modules_conf[-1] != '/':
410 # parse passed command line arguments
411 out = parse_cmdline(modules_dir, *sys.argv)
412 modules = out['modules']
413 if out['interval'] is not None:
414 interval = out['interval']
416 # configure environement to run modules
417 sys.path.append(modules_dir+"python_modules") # append path to directory with modules dependencies
420 charts = PythonCharts(interval, modules, modules_dir, modules_conf, disabled)
424 sys.stdout.write("DISABLE")
426 if __name__ == '__main__':