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