]> arthur.barton.de Git - netdata.git/blob - plugins.d/python.d.plugin
one job config
[netdata.git] / plugins.d / python.d.plugin
1 #!/usr/bin/env python3
2
3 import os
4 import sys
5 import time
6 try:
7     assert sys.version_info >= (3,1)
8     import importlib.machinery
9 except AssertionError:
10     sys.stderr.write('python.d.plugin: Not supported python version. Needed python >= 3.1\n')
11     sys.stdout.write('DISABLE\n')
12     sys.exit(1)
13 try:
14     import yaml
15 except ImportError:
16     sys.stderr.write('python.d.plugin: Cannot find yaml library\n')
17     sys.stdout.write('DISABLE\n')
18     sys.exit(1)
19
20 BASE_CONFIG = {'update_every' : 10,
21                'priority': 12345,
22                'retries' : 0}
23
24
25 class PythonCharts(object):
26     
27     def __init__(self,
28                  interval=None,
29                  modules=[],
30                  modules_path='../python.d/',
31                  modules_configs='../conf.d/',
32                  modules_disabled=[]):
33         self.first_run = True
34         # set configuration directory
35         self.configs = modules_configs
36
37         # load modules
38         modules = self._load_modules(modules_path,modules)
39         
40         # check if loaded modules are on disabled modules list
41         loaded_modules = [ m for m in modules if m.__name__ not in modules_disabled ]
42         
43         # load configuration files
44         configured_modules = self._load_configs(loaded_modules)
45
46         # good economy and prosperity:
47         self.jobs = self._create_jobs(configured_modules)
48
49
50     def _create_jobs(self,modules):
51     # module store a definition of Service class
52     # module store configuration in module.config
53     # configs are list of dicts or a dict
54     # one dict is one service
55     # iterate over list of modules and inside one loop iterate over configs
56         jobs = []
57         for module in modules:
58             for name in module.config:
59                 # register a new job
60                 conf = module.config[name]
61                 try:
62                     job = module.Service(configuration=conf, name=name)
63                 except Exception as e:
64                     debug(module.__name__ +
65                           ": Couldn't start job named " +
66                           str(name) + 
67                           ": " +
68                           str(e))
69                     return None
70                 else:
71                     # set execution_name (needed to plot run time graphs)
72                     job.execution_name = module.__name__
73                     if name is not None:
74                         job.execution_name += "_" + name
75                 jobs.append(job)
76         
77         return [j for j in jobs if j is not None]
78
79     def _import_module(self, path, name=None):
80     # try to import module using only its path
81         if name is None:
82             name = path.split('/')[-1]
83             if name[-9:] != ".chart.py":
84                 return None
85             name = name[:-9]
86         try:
87             return importlib.machinery.SourceFileLoader(name, path).load_module()
88         except Exception as e:
89             debug(str(e))
90             return None
91
92     def _load_modules(self, path, modules):
93         # check if plugin directory exists
94         if not os.path.isdir(path):
95             debug("cannot find charts directory ", path)
96             sys.stdout.write("DISABLE\n")
97             sys.exit(1)
98
99         # load modules
100         loaded = []
101         if len(modules) > 0:
102             for m in modules:
103                 mod = self._import_module(path + m + ".chart.py")
104                 if mod is not None:
105                     loaded.append(mod)
106         else:
107             # scan directory specified in path and load all modules from there
108             names = os.listdir(path)
109             for mod in names:
110                 m = self._import_module(path + mod)
111                 if m is not None:
112                     debug("loading chart: '" + path + mod + "'")
113                     loaded.append(m)
114         return loaded
115
116     def _load_configs(self,modules):
117     # function loads configuration files to modules
118         for mod in modules:
119             configfile = self.configs + mod.__name__ + ".conf"
120             if os.path.isfile(configfile):
121                 debug("loading chart options: '" + configfile + "'")
122                 try:
123                     setattr(mod,
124                             'config',
125                             self._parse_config(mod,read_config(configfile)))
126                 except Exception as e:
127                     debug("something went wrong while loading configuration",e)
128             else:
129                 debug(mod.__name__ +
130                       ": configuration file '" +
131                       configfile +
132                       "' not found. Using defaults.")
133         return modules
134
135     def _parse_config(self,module,config):
136         # get default values
137         defaults = {}
138         for key in BASE_CONFIG:
139             try:
140                 # get defaults from module config
141                 defaults[key] = int(config.pop(key))
142             except (KeyError,ValueError):
143                 try:
144                     # get defaults from module source code
145                     defaults[key] = getattr(module, key)
146                 except (KeyError,ValueError):
147                     # if above failed, get defaults from global dict
148                     defaults[key] = BASE_CONFIG[key]
149       
150         # check if there are dict in config dict
151         many_jobs = False
152         for name in config:
153             if type(config[name]) is dict:
154                 many_jobs = True
155                 break
156         
157         # assign variables needed by supervisor to every job configuration
158         if many_jobs:
159             for name in config:
160                 for key in defaults:
161                     if key not in config[name]:
162                         config[name][key] = defaults[key]
163         # if only one job is needed, values doesn't have to be in dict (in YAML)
164         else:
165             config = {None: config.copy()}
166             config[None].update(defaults)
167             
168         # return dictionary of jobs where every job has BASE_CONFIG variables
169         return config
170
171     def _stop(self, job, reason=None): #FIXME test if Service has __name__
172     # modifies self.jobs
173         self.jobs.remove(job)
174         if reason is None:
175             return
176         elif reason[:3] == "no ":
177             debug("chart '" +
178                   job.execution_name,
179                   "' does not seem to have " +
180                   reason[3:] +
181                   "() function. Disabling it.")
182         elif reason[:7] == "failed ":
183             debug("chart '" +
184                   job.execution_name + "' " +
185                   reason[7:] +
186                   "() function reports failure.")
187         elif reason[:13] == "configuration":
188             debug(job.execution_name,
189                   "configuration file '" +
190                   self.configs +
191                   job.execution_name +
192                   ".conf' not found. Using defaults.")
193         elif reason[:11] == "misbehaving":
194             debug(job.execution_name, "is "+reason)
195
196     def check(self):
197     # try to execute check() on every job
198         for job in self.jobs:
199             try:
200                 if not job.check():
201                     self._stop(job, "failed check")
202             except AttributeError:
203                 self._stop(job, "no check")
204             except (UnboundLocalError, Exception) as e:
205                 self._stop(job, "misbehaving. Reason: " + str(e))
206         print("CHECK OK")
207
208     def create(self):
209     # try to execute create() on every job
210         for job in self.jobs:
211             try:
212                 if not job.create():
213                     self._stop(job, "failed create")
214                 else:
215                     chart = job.execution_name
216                     sys.stdout.write(
217                         "CHART netdata.plugin_pythond_" +
218                         chart +
219                         " '' 'Execution time for " +
220                         chart +
221                         " plugin' 'milliseconds / run' python.d netdata.plugin_python area 145000 " +
222                         str(job.timetable['freq']) +
223                         '\n')
224                     sys.stdout.write("DIMENSION run_time 'run time' absolute 1 1\n\n")
225                     sys.stdout.flush()
226             except AttributeError:
227                 self._stop(job, "no create")
228             except (UnboundLocalError, Exception) as e:
229                 self._stop(job, "misbehaving. Reason: " + str(e))
230         print("CREATE OK")
231
232     def _update_job(self, job):
233     # try to execute update() on every job and draw run time graph 
234         t_start = time.time()
235         # check if it is time to execute job update() function
236         if job.timetable['next'] > t_start:
237             return
238         try:
239             if self.first_run:
240                 since_last = 0
241             else:
242                 since_last = int((t_start - job.timetable['last']) * 1000000)
243             if not job.update(since_last):
244                 self._stop(job, "update failed")
245                 return
246         except AttributeError:
247             self._stop(job, "no update")
248             return
249         except (UnboundLocalError, Exception) as e:
250             self._stop(job, "misbehaving. Reason: " + str(e))
251             return
252         t_end = time.time()
253         job.timetable['next'] = t_end - (t_end % job.timetable['freq']) + job.timetable['freq']
254         # draw performance graph
255         if self.first_run:
256             dt = 0
257         else:
258             dt = int((t_end - job.timetable['last']) * 1000000)
259         sys.stdout.write("BEGIN netdata.plugin_pythond_"+job.execution_name+" "+str(since_last)+'\n')
260         sys.stdout.write("SET run_time = " + str(int((t_end - t_start) * 1000)) + '\n')
261         sys.stdout.write("END\n")
262         sys.stdout.flush()
263         job.timetable['last'] = t_start
264         self.first_run = False
265
266     def update(self):
267     # run updates (this will stay forever and ever and ever forever and ever it'll be the one...)
268         self.first_run = True
269         while True:
270             t_begin = time.time()
271             next_runs = []
272             for job in self.jobs:
273                 self._update_job(job)
274                 try:
275                     next_runs.append(job.timetable['next'])
276                 except KeyError:
277                     pass
278             if len(next_runs) == 0:
279                 debug("No plugins loaded")
280                 sys.stdout.write("DISABLE\n")
281                 sys.exit(1)
282             time.sleep(min(next_runs) - time.time())
283
284
285 def read_config(path):
286     try:
287         with open(path, 'r') as stream:
288             config = yaml.load(stream)
289     except IsADirectoryError:
290         debug(str(path), "is a directory")
291         return None
292     except yaml.YAMLError as e:
293         debug(str(path), "is malformed:", e)
294         return None
295     return config
296
297
298 def debug(*args):
299     if not DEBUG_FLAG:
300         return
301     sys.stderr.write(PROGRAM + ":")
302     for i in args:
303         sys.stderr.write(" " + str(i))
304     sys.stderr.write("\n")
305     sys.stderr.flush()
306
307
308 def parse_cmdline(directory, *commands):
309     # TODO number -> interval
310     global DEBUG_FLAG
311     DEBUG_FLAG = False
312     interval = None
313
314     mods = []
315     for cmd in commands[1:]:
316         if cmd == "check":
317             pass
318         elif cmd == "debug" or cmd == "all":
319             DEBUG_FLAG = True
320             # redirect stderr to stdout?
321         elif os.path.isfile(directory + cmd + ".chart.py") or os.path.isfile(directory + cmd):
322             DEBUG_FLAG = True
323             mods.append(cmd.replace(".chart.py", ""))
324         else:
325             try:
326                 interval = int(cmd)
327             except ValueError:
328                 pass
329
330     debug("started from", commands[0], "with options:", *commands[1:])
331     if len(mods) == 0 and DEBUG_FLAG is False:
332         interval = None
333
334     return {'interval': interval,
335             'modules': mods}
336
337
338 # if __name__ == '__main__':
339 def run():
340     global DEBUG_FLAG, PROGRAM
341     DEBUG_FLAG = True
342     PROGRAM = sys.argv[0].split('/')[-1].split('.plugin')[0]
343     # parse env variables
344     # https://github.com/firehol/netdata/wiki/External-Plugins#environment-variables
345     main_dir = os.getenv('NETDATA_PLUGINS_DIR',
346                          os.path.abspath(__file__).strip("python.d.plugin.py"))
347     config_dir = os.getenv('NETDATA_CONFIG_DIR', "/etc/netdata/")
348     interval = os.getenv('NETDATA_UPDATE_EVERY', None)
349
350     # read configuration file
351     disabled = []
352     if config_dir[-1] != '/':
353         config_dir += '/'
354     configfile = config_dir + "python.d.conf"
355
356     try:
357         conf = read_config(configfile)
358         try:
359             if str(conf['enable']) is False:
360                 debug("disabled in configuration file")
361                 sys.stdout.write("DISABLE\n")
362                 sys.exit(1)
363         except (KeyError, TypeError):
364             pass
365         try:
366             modules_conf = conf['plugins_config_dir']
367         except KeyError:
368             modules_conf = config_dir + "python.d/"  # default configuration directory
369         try:
370             modules_dir = conf['plugins_dir']
371         except KeyError:
372             modules_dir = main_dir.replace("plugins.d", "python.d")
373         try:
374             interval = conf['interval']
375         except KeyError:
376             pass  # use default interval from NETDATA_UPDATE_EVERY
377         try:
378             DEBUG_FLAG = conf['debug']
379         except KeyError:
380             pass
381         for k, v in conf.items():
382             if k in ("plugins_config_dir", "plugins_dir", "interval", "debug"):
383                 continue
384             if v is False:
385                 disabled.append(k)
386     except FileNotFoundError:
387         modules_conf = config_dir
388         modules_dir = main_dir.replace("plugins.d", "python.d")
389
390     # directories should end with '/'
391     if modules_dir[-1] != '/':
392         modules_dir += "/"
393     if modules_conf[-1] != '/':
394         modules_conf += "/"
395
396     # parse passed command line arguments
397     out = parse_cmdline(modules_dir, *sys.argv)
398     modules = out['modules']
399     if out['interval'] is not None:
400         interval = out['interval']
401     
402     # configure environement to run modules
403     sys.path.append(modules_dir+"python_modules") # append path to directory with modules dependencies
404     
405     # run plugins
406     charts = PythonCharts(interval, modules, modules_dir, modules_conf, disabled)
407     charts.check()
408     charts.create()
409     charts.update()
410
411 if __name__ == '__main__':
412     run()