]> git.ipfire.org Git - collecty.git/blame - src/collecty/plugins/base.py
Add cpufreq plugin
[collecty.git] / src / collecty / plugins / base.py
CommitLineData
eed405de
MT
1#!/usr/bin/python
2###############################################################################
3# #
4# collecty - A system statistics collection daemon for IPFire #
5# Copyright (C) 2012 IPFire development team #
6# #
7# This program is free software: you can redistribute it and/or modify #
8# it under the terms of the GNU General Public License as published by #
9# the Free Software Foundation, either version 3 of the License, or #
10# (at your option) any later version. #
11# #
12# This program is distributed in the hope that it will be useful, #
13# but WITHOUT ANY WARRANTY; without even the implied warranty of #
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
15# GNU General Public License for more details. #
16# #
17# You should have received a copy of the GNU General Public License #
18# along with this program. If not, see <http://www.gnu.org/licenses/>. #
19# #
20###############################################################################
21
965a9c51
MT
22from __future__ import division
23
72364063 24import datetime
4ac0cdf0 25import logging
965a9c51 26import math
4ac0cdf0
MT
27import os
28import rrdtool
c968f6d9 29import tempfile
49ce926e 30import threading
4ac0cdf0
MT
31import time
32
4ac0cdf0 33from ..constants import *
eed405de
MT
34from ..i18n import _
35
5d140577
MT
36_plugins = {}
37
38def get():
39 """
40 Returns a list with all automatically registered plugins.
41 """
42 return _plugins.values()
43
4be39bf9
MT
44class Timer(object):
45 def __init__(self, timeout, heartbeat=1):
46 self.timeout = timeout
47 self.heartbeat = heartbeat
48
e746a56e
MT
49 self.delay = 0
50
4be39bf9
MT
51 self.reset()
52
e746a56e 53 def reset(self, delay=0):
4be39bf9
MT
54 # Save start time.
55 self.start = time.time()
56
e746a56e
MT
57 self.delay = delay
58
4be39bf9
MT
59 # Has this timer been killed?
60 self.killed = False
61
62 @property
63 def elapsed(self):
e746a56e 64 return time.time() - self.start - self.delay
4be39bf9
MT
65
66 def cancel(self):
67 self.killed = True
68
69 def wait(self):
70 while self.elapsed < self.timeout and not self.killed:
71 time.sleep(self.heartbeat)
72
73 return self.elapsed > self.timeout
74
75
49c1b8fd 76class Plugin(object):
4ac0cdf0
MT
77 # The name of this plugin.
78 name = None
79
80 # A description for this plugin.
81 description = None
82
b1ea4956
MT
83 # Templates which can be used to generate a graph out of
84 # the data from this data source.
85 templates = []
86
72364063
MT
87 # The default interval for all plugins
88 interval = 60
4ac0cdf0 89
5d140577
MT
90 # Automatically register all providers.
91 class __metaclass__(type):
92 def __init__(plugin, name, bases, dict):
93 type.__init__(plugin, name, bases, dict)
94
95 # The main class from which is inherited is not registered
96 # as a plugin.
97 if name == "Plugin":
98 return
99
100 if not all((plugin.name, plugin.description)):
101 raise RuntimeError(_("Plugin is not properly configured: %s") \
102 % plugin)
103
104 _plugins[plugin.name] = plugin
105
eed405de 106 def __init__(self, collecty, **kwargs):
eed405de
MT
107 self.collecty = collecty
108
4ac0cdf0
MT
109 # Check if this plugin was configured correctly.
110 assert self.name, "Name of the plugin is not set: %s" % self.name
111 assert self.description, "Description of the plugin is not set: %s" % self.description
4ac0cdf0
MT
112
113 # Initialize the logger.
114 self.log = logging.getLogger("collecty.plugins.%s" % self.name)
115 self.log.propagate = 1
eed405de 116
eed405de
MT
117 self.data = []
118
269f74cd
MT
119 # Run some custom initialization.
120 self.init(**kwargs)
121
0ee0c42d 122 self.log.debug(_("Successfully initialized %s") % self.__class__.__name__)
4ac0cdf0 123
73db5226 124 @property
72364063 125 def path(self):
73db5226 126 """
72364063
MT
127 Returns the name of the sub directory in which all RRD files
128 for this plugin should be stored in.
73db5226
MT
129 """
130 return self.name
131
72364063
MT
132 ### Basic methods
133
134 def init(self, **kwargs):
4ac0cdf0 135 """
72364063 136 Do some custom initialization stuff here.
4ac0cdf0 137 """
72364063
MT
138 pass
139
140 def collect(self):
141 """
142 Gathers the statistical data, this plugin collects.
143 """
144 time_start = time.time()
145
146 # Run through all objects of this plugin and call the collect method.
147 for o in self.objects:
148 now = datetime.datetime.utcnow()
149 try:
150 result = o.collect()
f648421a
MT
151
152 if isinstance(result, tuple) or isinstance(result, list):
153 result = ":".join(("%s" % e for e in result))
72364063
MT
154 except:
155 self.log.warning(_("Unhandled exception in %s.collect()") % o, exc_info=True)
156 continue
157
158 if not result:
159 self.log.warning(_("Received empty result: %s") % o)
160 continue
161
162 self.log.debug(_("Collected %s: %s") % (o, result))
163
164 # Add the object to the write queue so that the data is written
165 # to the databases later.
166 self.collecty.write_queue.add(o, now, result)
167
168 # Returns the time this function took to complete.
49c1b8fd 169 delay = time.time() - time_start
72364063 170
49c1b8fd
MT
171 # Log some warning when a collect method takes too long to return some data
172 if delay >= 60:
173 self.log.warning(_("A worker thread was stalled for %.4fs") % delay)
4ac0cdf0 174
c968f6d9
MT
175 def get_object(self, id):
176 for object in self.objects:
177 if not object.id == id:
178 continue
179
180 return object
181
0308c0f3 182 def get_template(self, template_name, object_id):
c968f6d9
MT
183 for template in self.templates:
184 if not template.name == template_name:
185 continue
186
0308c0f3 187 return template(self, object_id)
c968f6d9
MT
188
189 def generate_graph(self, template_name, object_id="default", **kwargs):
0308c0f3 190 template = self.get_template(template_name, object_id=object_id)
c968f6d9
MT
191 if not template:
192 raise RuntimeError("Could not find template %s" % template_name)
193
194 time_start = time.time()
195
0308c0f3 196 graph = template.generate_graph(**kwargs)
c968f6d9
MT
197
198 duration = time.time() - time_start
0ee0c42d 199 self.log.debug(_("Generated graph %s in %.1fms") \
c968f6d9
MT
200 % (template, duration * 1000))
201
202 return graph
203
72364063
MT
204
205class Object(object):
206 # The schema of the RRD database.
207 rrd_schema = None
208
209 # RRA properties.
210 rra_types = ["AVERAGE", "MIN", "MAX"]
211 rra_timespans = [3600, 86400, 604800, 2678400, 31622400]
212 rra_rows = 2880
213
214 def __init__(self, plugin, *args, **kwargs):
215 self.plugin = plugin
216
217 # Indicates if this object has collected its data
218 self.collected = False
219
220 # Initialise this object
221 self.init(*args, **kwargs)
222
223 # Create the database file.
224 self.create()
225
226 def __repr__(self):
227 return "<%s>" % self.__class__.__name__
4ac0cdf0 228
965a9c51 229 @property
72364063
MT
230 def collecty(self):
231 return self.plugin.collecty
965a9c51 232
881751ed 233 @property
72364063
MT
234 def log(self):
235 return self.plugin.log
236
237 @property
238 def id(self):
239 """
240 Returns a UNIQUE identifier for this object. As this is incorporated
241 into the path of RRD file, it must only contain ASCII characters.
242 """
243 raise NotImplementedError
881751ed 244
4ac0cdf0
MT
245 @property
246 def file(self):
247 """
248 The absolute path to the RRD file of this plugin.
249 """
57797e47
MT
250 filename = self._normalise_filename("%s.rrd" % self.id)
251
252 return os.path.join(DATABASE_DIR, self.plugin.path, filename)
253
254 @staticmethod
255 def _normalise_filename(filename):
256 # Convert the filename into ASCII characters only
257 filename = filename.encode("ascii", "ignore")
258
259 # Replace any spaces by dashes
260 filename = filename.replace(" ", "-")
261
262 return filename
72364063
MT
263
264 ### Basic methods
265
266 def init(self, *args, **kwargs):
267 """
268 Do some custom initialization stuff here.
269 """
270 pass
eed405de 271
4ac0cdf0
MT
272 def create(self):
273 """
274 Creates an empty RRD file with the desired data structures.
275 """
276 # Skip if the file does already exist.
277 if os.path.exists(self.file):
278 return
eed405de 279
4ac0cdf0
MT
280 dirname = os.path.dirname(self.file)
281 if not os.path.exists(dirname):
282 os.makedirs(dirname)
eed405de 283
965a9c51 284 # Create argument list.
ff0bbd88 285 args = self.get_rrd_schema()
965a9c51
MT
286
287 rrdtool.create(self.file, *args)
eed405de 288
4ac0cdf0 289 self.log.debug(_("Created RRD file %s.") % self.file)
dadb8fb0
MT
290 for arg in args:
291 self.log.debug(" %s" % arg)
eed405de 292
72364063
MT
293 def info(self):
294 return rrdtool.info(self.file)
295
296 @property
297 def stepsize(self):
298 return self.plugin.interval
299
300 @property
301 def heartbeat(self):
302 return self.stepsize * 2
303
965a9c51
MT
304 def get_rrd_schema(self):
305 schema = [
306 "--step", "%s" % self.stepsize,
307 ]
308 for line in self.rrd_schema:
309 if line.startswith("DS:"):
310 try:
311 (prefix, name, type, lower_limit, upper_limit) = line.split(":")
312
313 line = ":".join((
314 prefix,
315 name,
316 type,
881751ed 317 "%s" % self.heartbeat,
965a9c51
MT
318 lower_limit,
319 upper_limit
320 ))
321 except ValueError:
322 pass
323
324 schema.append(line)
325
326 xff = 0.1
327
328 cdp_length = 0
329 for rra_timespan in self.rra_timespans:
330 if (rra_timespan / self.stepsize) < self.rra_rows:
331 rra_timespan = self.stepsize * self.rra_rows
332
333 if cdp_length == 0:
334 cdp_length = 1
335 else:
336 cdp_length = rra_timespan // (self.rra_rows * self.stepsize)
337
338 cdp_number = math.ceil(rra_timespan / (cdp_length * self.stepsize))
339
340 for rra_type in self.rra_types:
341 schema.append("RRA:%s:%.10f:%d:%d" % \
342 (rra_type, xff, cdp_length, cdp_number))
343
344 return schema
345
72364063
MT
346 def execute(self):
347 if self.collected:
348 raise RuntimeError("This object has already collected its data")
eed405de 349
72364063
MT
350 self.collected = True
351 self.now = datetime.datetime.utcnow()
4ac0cdf0 352
72364063
MT
353 # Call the collect
354 result = self.collect()
4ac0cdf0 355
72364063 356 def commit(self):
4ac0cdf0 357 """
72364063 358 Will commit the collected data to the database.
4ac0cdf0 359 """
72364063 360 # Make sure that the RRD database has been created
dadb8fb0
MT
361 self.create()
362
b1ea4956
MT
363
364class GraphTemplate(object):
365 # A unique name to identify this graph template.
366 name = None
367
f181246a
MT
368 # Headline of the graph image
369 graph_title = None
370
371 # Vertical label of the graph
372 graph_vertical_label = None
373
374 # Limits
375 lower_limit = None
376 upper_limit = None
377
b1ea4956
MT
378 # Instructions how to create the graph.
379 rrd_graph = None
380
381 # Extra arguments passed to rrdgraph.
382 rrd_graph_args = []
383
c968f6d9
MT
384 intervals = {
385 None : "-3h",
386 "hour" : "-1h",
387 "day" : "-25h",
388 "week" : "-360h",
389 "year" : "-365d",
390 }
391
392 # Default dimensions for this graph
393 height = GRAPH_DEFAULT_HEIGHT
394 width = GRAPH_DEFAULT_WIDTH
395
0308c0f3 396 def __init__(self, plugin, object_id):
c968f6d9
MT
397 self.plugin = plugin
398
0308c0f3
MT
399 # Get all required RRD objects
400 self.object_id = object_id
401
402 # Get the main object
403 self.object = self.get_object(self.object_id)
404
c968f6d9
MT
405 def __repr__(self):
406 return "<%s>" % self.__class__.__name__
b1ea4956
MT
407
408 @property
409 def collecty(self):
c968f6d9 410 return self.plugin.collecty
b1ea4956 411
c968f6d9
MT
412 @property
413 def log(self):
414 return self.plugin.log
415
5913a52c
MT
416 def _make_command_line(self, interval, format=DEFAULT_IMAGE_FORMAT,
417 width=None, height=None):
c968f6d9
MT
418 args = []
419
420 args += GRAPH_DEFAULT_ARGUMENTS
421
422 args += [
5913a52c 423 "--imgformat", format,
c968f6d9
MT
424 "--height", "%s" % (height or self.height),
425 "--width", "%s" % (width or self.width),
73db5226 426 ]
eed405de 427
c968f6d9 428 args += self.rrd_graph_args
eed405de 429
f181246a
MT
430 # Graph title
431 if self.graph_title:
432 args += ["--title", self.graph_title]
433
434 # Vertical label
435 if self.graph_vertical_label:
436 args += ["--vertical-label", self.graph_vertical_label]
437
438 if self.lower_limit is not None or self.upper_limit is not None:
439 # Force to honour the set limits
440 args.append("--rigid")
441
442 if self.lower_limit is not None:
443 args += ["--lower-limit", self.lower_limit]
444
445 if self.upper_limit is not None:
446 args += ["--upper-limit", self.upper_limit]
447
c968f6d9 448 # Add interval
eed405de 449 args.append("--start")
c968f6d9 450
b1ea4956 451 try:
c968f6d9 452 args.append(self.intervals[interval])
b1ea4956 453 except KeyError:
c968f6d9
MT
454 args.append(str(interval))
455
456 return args
457
0308c0f3
MT
458 def get_object(self, *args, **kwargs):
459 return self.plugin.get_object(*args, **kwargs)
460
461 def get_object_table(self):
c968f6d9 462 return {
0308c0f3 463 "file" : self.object,
c968f6d9
MT
464 }
465
fc359f4d
MT
466 @property
467 def object_table(self):
468 if not hasattr(self, "_object_table"):
469 self._object_table = self.get_object_table()
470
471 return self._object_table
472
0308c0f3 473 def get_object_files(self):
c968f6d9
MT
474 files = {}
475
fc359f4d 476 for id, obj in self.object_table.items():
c968f6d9
MT
477 files[id] = obj.file
478
479 return files
480
0308c0f3 481 def generate_graph(self, interval=None, **kwargs):
c968f6d9
MT
482 args = self._make_command_line(interval, **kwargs)
483
484 self.log.info(_("Generating graph %s") % self)
485 self.log.debug(" args: %s" % args)
486
0308c0f3 487 object_files = self.get_object_files()
eed405de 488
73db5226 489 for item in self.rrd_graph:
eed405de 490 try:
c968f6d9 491 args.append(item % object_files)
eed405de
MT
492 except TypeError:
493 args.append(item)
494
0308c0f3
MT
495 self.log.debug(" %s" % args[-1])
496
c968f6d9
MT
497 return self.write_graph(*args)
498
499 def write_graph(self, *args):
5913a52c
MT
500 # Convert all arguments to string
501 args = [str(e) for e in args]
502
c968f6d9
MT
503 with tempfile.NamedTemporaryFile() as f:
504 rrdtool.graph(f.name, *args)
505
506 # Get back to the beginning of the file
507 f.seek(0)
508
509 # Return all the content
510 return f.read()