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