]> git.ipfire.org Git - collecty.git/blame - src/collecty/plugins/base.py
Remove some unused code
[collecty.git] / src / collecty / plugins / base.py
CommitLineData
f37913e8 1#!/usr/bin/python3
eed405de
MT
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
72364063 22import datetime
4ac0cdf0 23import logging
965a9c51 24import math
4ac0cdf0 25import os
cd8bba0b 26import re
4ac0cdf0 27import rrdtool
c968f6d9 28import tempfile
49ce926e 29import threading
4ac0cdf0 30import time
f37913e8 31import unicodedata
4ac0cdf0 32
429ba506 33from .. import locales
4d9ed86e 34from .. import util
4ac0cdf0 35from ..constants import *
eed405de
MT
36from ..i18n import _
37
cd8bba0b
MT
38DEF_MATCH = re.compile(r"C?DEF:([A-Za-z0-9_]+)=")
39
4be39bf9
MT
40class Timer(object):
41 def __init__(self, timeout, heartbeat=1):
42 self.timeout = timeout
43 self.heartbeat = heartbeat
44
e746a56e
MT
45 self.delay = 0
46
4be39bf9
MT
47 self.reset()
48
e746a56e 49 def reset(self, delay=0):
4be39bf9
MT
50 # Save start time.
51 self.start = time.time()
52
e746a56e
MT
53 self.delay = delay
54
4be39bf9
MT
55 # Has this timer been killed?
56 self.killed = False
57
58 @property
59 def elapsed(self):
e746a56e 60 return time.time() - self.start - self.delay
4be39bf9
MT
61
62 def cancel(self):
63 self.killed = True
64
65 def wait(self):
66 while self.elapsed < self.timeout and not self.killed:
67 time.sleep(self.heartbeat)
68
69 return self.elapsed > self.timeout
70
71
cb1ccb4f
MT
72class Environment(object):
73 """
74 Sets the correct environment for rrdtool to create
75 localised graphs and graphs in the correct timezone.
76 """
77 def __init__(self, timezone, locale):
78 # Build the new environment
79 self.new_environment = {
80 "TZ" : timezone or DEFAULT_TIMEZONE,
81 }
82
83 for k in ("LANG", "LC_ALL"):
84 self.new_environment[k] = locale or DEFAULT_LOCALE
85
86 def __enter__(self):
87 # Save the current environment
88 self.old_environment = {}
89 for k in self.new_environment:
90 self.old_environment[k] = os.environ.get(k, None)
91
92 # Apply the new one
93 os.environ.update(self.new_environment)
94
95 def __exit__(self, type, value, traceback):
96 # Roll back to the previous environment
97 for k, v in self.old_environment.items():
98 if v is None:
99 try:
100 del os.environ[k]
101 except KeyError:
102 pass
103 else:
104 os.environ[k] = v
105
106
f37913e8
MT
107class PluginRegistration(type):
108 plugins = {}
109
110 def __init__(plugin, name, bases, dict):
111 type.__init__(plugin, name, bases, dict)
112
113 # The main class from which is inherited is not registered
114 # as a plugin.
115 if name == "Plugin":
116 return
117
118 if not all((plugin.name, plugin.description)):
119 raise RuntimeError(_("Plugin is not properly configured: %s") % plugin)
120
121 PluginRegistration.plugins[plugin.name] = plugin
122
123
124def get():
125 """
126 Returns a list with all automatically registered plugins.
127 """
128 return PluginRegistration.plugins.values()
129
130class Plugin(object, metaclass=PluginRegistration):
4ac0cdf0
MT
131 # The name of this plugin.
132 name = None
133
134 # A description for this plugin.
135 description = None
136
b1ea4956
MT
137 # Templates which can be used to generate a graph out of
138 # the data from this data source.
139 templates = []
140
72364063
MT
141 # The default interval for all plugins
142 interval = 60
4ac0cdf0 143
eed405de 144 def __init__(self, collecty, **kwargs):
eed405de
MT
145 self.collecty = collecty
146
4ac0cdf0
MT
147 # Check if this plugin was configured correctly.
148 assert self.name, "Name of the plugin is not set: %s" % self.name
149 assert self.description, "Description of the plugin is not set: %s" % self.description
4ac0cdf0
MT
150
151 # Initialize the logger.
152 self.log = logging.getLogger("collecty.plugins.%s" % self.name)
153 self.log.propagate = 1
eed405de 154
eed405de
MT
155 self.data = []
156
269f74cd
MT
157 # Run some custom initialization.
158 self.init(**kwargs)
159
0ee0c42d 160 self.log.debug(_("Successfully initialized %s") % self.__class__.__name__)
4ac0cdf0 161
73db5226 162 @property
72364063 163 def path(self):
73db5226 164 """
72364063
MT
165 Returns the name of the sub directory in which all RRD files
166 for this plugin should be stored in.
73db5226
MT
167 """
168 return self.name
169
72364063
MT
170 ### Basic methods
171
172 def init(self, **kwargs):
4ac0cdf0 173 """
72364063 174 Do some custom initialization stuff here.
4ac0cdf0 175 """
72364063
MT
176 pass
177
178 def collect(self):
179 """
180 Gathers the statistical data, this plugin collects.
181 """
182 time_start = time.time()
183
184 # Run through all objects of this plugin and call the collect method.
185 for o in self.objects:
186 now = datetime.datetime.utcnow()
187 try:
188 result = o.collect()
f648421a 189
a9af411f 190 result = self._format_result(result)
72364063
MT
191 except:
192 self.log.warning(_("Unhandled exception in %s.collect()") % o, exc_info=True)
193 continue
194
195 if not result:
196 self.log.warning(_("Received empty result: %s") % o)
197 continue
198
199 self.log.debug(_("Collected %s: %s") % (o, result))
200
201 # Add the object to the write queue so that the data is written
202 # to the databases later.
203 self.collecty.write_queue.add(o, now, result)
204
205 # Returns the time this function took to complete.
49c1b8fd 206 delay = time.time() - time_start
72364063 207
49c1b8fd
MT
208 # Log some warning when a collect method takes too long to return some data
209 if delay >= 60:
210 self.log.warning(_("A worker thread was stalled for %.4fs") % delay)
4ac0cdf0 211
a9af411f
MT
212 @staticmethod
213 def _format_result(result):
214 if not isinstance(result, tuple) and not isinstance(result, list):
215 return result
216
217 # Replace all Nones by NaN
218 s = []
219
220 for e in result:
221 if e is None:
222 e = "NaN"
223
224 # Format as string
225 e = "%s" % e
226
227 s.append(e)
228
229 return ":".join(s)
230
c968f6d9
MT
231 def get_object(self, id):
232 for object in self.objects:
233 if not object.id == id:
234 continue
235
236 return object
237
429ba506 238 def get_template(self, template_name, object_id, locale=None, timezone=None):
c968f6d9
MT
239 for template in self.templates:
240 if not template.name == template_name:
241 continue
242
429ba506 243 return template(self, object_id, locale=locale, timezone=timezone)
c968f6d9 244
429ba506
MT
245 def generate_graph(self, template_name, object_id="default",
246 timezone=None, locale=None, **kwargs):
247 template = self.get_template(template_name, object_id=object_id,
248 timezone=timezone, locale=locale)
c968f6d9
MT
249 if not template:
250 raise RuntimeError("Could not find template %s" % template_name)
251
252 time_start = time.time()
253
0308c0f3 254 graph = template.generate_graph(**kwargs)
c968f6d9
MT
255
256 duration = time.time() - time_start
0ee0c42d 257 self.log.debug(_("Generated graph %s in %.1fms") \
c968f6d9
MT
258 % (template, duration * 1000))
259
260 return graph
261
a3864812
MT
262 def graph_info(self, template_name, object_id="default",
263 timezone=None, locale=None, **kwargs):
264 template = self.get_template(template_name, object_id=object_id,
265 timezone=timezone, locale=locale)
266 if not template:
267 raise RuntimeError("Could not find template %s" % template_name)
268
269 return template.graph_info()
270
8ee5a71a
MT
271 def last_update(self, object_id="default"):
272 object = self.get_object(object_id)
273 if not object:
274 raise RuntimeError("Could not find object %s" % object_id)
275
276 return object.last_update()
277
72364063
MT
278
279class Object(object):
280 # The schema of the RRD database.
281 rrd_schema = None
282
283 # RRA properties.
418174a4
MT
284 rra_types = ("AVERAGE", "MIN", "MAX")
285 rra_timespans = (
286 ("1m", "10d"),
287 ("1h", "18M"),
288 ("1d", "5y"),
289 )
72364063
MT
290
291 def __init__(self, plugin, *args, **kwargs):
292 self.plugin = plugin
293
294 # Indicates if this object has collected its data
295 self.collected = False
296
297 # Initialise this object
298 self.init(*args, **kwargs)
299
300 # Create the database file.
301 self.create()
302
303 def __repr__(self):
304 return "<%s>" % self.__class__.__name__
4ac0cdf0 305
965a9c51 306 @property
72364063
MT
307 def collecty(self):
308 return self.plugin.collecty
965a9c51 309
881751ed 310 @property
72364063
MT
311 def log(self):
312 return self.plugin.log
313
314 @property
315 def id(self):
316 """
317 Returns a UNIQUE identifier for this object. As this is incorporated
318 into the path of RRD file, it must only contain ASCII characters.
319 """
320 raise NotImplementedError
881751ed 321
4ac0cdf0
MT
322 @property
323 def file(self):
324 """
325 The absolute path to the RRD file of this plugin.
326 """
57797e47
MT
327 filename = self._normalise_filename("%s.rrd" % self.id)
328
329 return os.path.join(DATABASE_DIR, self.plugin.path, filename)
330
331 @staticmethod
332 def _normalise_filename(filename):
333 # Convert the filename into ASCII characters only
f37913e8 334 filename = unicodedata.normalize("NFKC", filename)
57797e47
MT
335
336 # Replace any spaces by dashes
337 filename = filename.replace(" ", "-")
338
339 return filename
72364063
MT
340
341 ### Basic methods
342
343 def init(self, *args, **kwargs):
344 """
345 Do some custom initialization stuff here.
346 """
347 pass
eed405de 348
4ac0cdf0
MT
349 def create(self):
350 """
351 Creates an empty RRD file with the desired data structures.
352 """
353 # Skip if the file does already exist.
354 if os.path.exists(self.file):
355 return
eed405de 356
4ac0cdf0
MT
357 dirname = os.path.dirname(self.file)
358 if not os.path.exists(dirname):
359 os.makedirs(dirname)
eed405de 360
965a9c51 361 # Create argument list.
ff0bbd88 362 args = self.get_rrd_schema()
965a9c51
MT
363
364 rrdtool.create(self.file, *args)
eed405de 365
4ac0cdf0 366 self.log.debug(_("Created RRD file %s.") % self.file)
dadb8fb0
MT
367 for arg in args:
368 self.log.debug(" %s" % arg)
eed405de 369
72364063
MT
370 def info(self):
371 return rrdtool.info(self.file)
372
8ee5a71a
MT
373 def last_update(self):
374 """
375 Returns a dictionary with the timestamp and
376 data set of the last database update.
377 """
378 return {
379 "dataset" : self.last_dataset,
380 "timestamp" : self.last_updated,
381 }
382
383 def _last_update(self):
384 return rrdtool.lastupdate(self.file)
385
386 @property
387 def last_updated(self):
388 """
389 Returns the timestamp when this database was last updated
390 """
391 lu = self._last_update()
392
393 if lu:
394 return lu.get("date")
395
396 @property
397 def last_dataset(self):
398 """
399 Returns the latest dataset in the database
400 """
401 lu = self._last_update()
402
403 if lu:
404 return lu.get("ds")
405
72364063
MT
406 @property
407 def stepsize(self):
408 return self.plugin.interval
409
410 @property
411 def heartbeat(self):
412 return self.stepsize * 2
413
965a9c51
MT
414 def get_rrd_schema(self):
415 schema = [
416 "--step", "%s" % self.stepsize,
417 ]
418 for line in self.rrd_schema:
419 if line.startswith("DS:"):
420 try:
421 (prefix, name, type, lower_limit, upper_limit) = line.split(":")
422
423 line = ":".join((
424 prefix,
425 name,
426 type,
881751ed 427 "%s" % self.heartbeat,
965a9c51
MT
428 lower_limit,
429 upper_limit
430 ))
431 except ValueError:
432 pass
433
434 schema.append(line)
435
436 xff = 0.1
437
418174a4
MT
438 for steps, rows in self.rra_timespans:
439 for type in self.rra_types:
440 schema.append("RRA:%s:%s:%s:%s" % (type, xff, steps, rows))
965a9c51
MT
441
442 return schema
443
e0437caa
MT
444 @property
445 def rrd_schema_names(self):
446 ret = []
cd8bba0b
MT
447
448 for line in self.rrd_schema:
e0437caa
MT
449 (prefix, name, type, lower_limit, upper_limit) = line.split(":")
450 ret.append(name)
451
452 return ret
453
454 def make_rrd_defs(self, prefix=None):
455 defs = []
cd8bba0b 456
e0437caa 457 for name in self.rrd_schema_names:
cd8bba0b
MT
458 if prefix:
459 p = "%s_%s" % (prefix, name)
460 else:
461 p = name
462
463 defs += [
464 "DEF:%s=%s:%s:AVERAGE" % (p, self.file, name),
465 ]
466
467 return defs
468
4d9ed86e
MT
469 def get_stddev(self, interval=None):
470 args = self.make_rrd_defs()
471
472 # Add the correct interval
473 args += ["--start", util.make_interval(interval)]
474
475 for name in self.rrd_schema_names:
476 args += [
477 "VDEF:%s_stddev=%s,STDEV" % (name, name),
478 "PRINT:%s_stddev:%%lf" % name,
479 ]
480
481 x, y, vals = rrdtool.graph("/dev/null", *args)
482 return dict(zip(self.rrd_schema_names, vals))
483
72364063
MT
484 def execute(self):
485 if self.collected:
486 raise RuntimeError("This object has already collected its data")
eed405de 487
72364063
MT
488 self.collected = True
489 self.now = datetime.datetime.utcnow()
4ac0cdf0 490
72364063
MT
491 # Call the collect
492 result = self.collect()
4ac0cdf0 493
72364063 494 def commit(self):
4ac0cdf0 495 """
72364063 496 Will commit the collected data to the database.
4ac0cdf0 497 """
72364063 498 # Make sure that the RRD database has been created
dadb8fb0
MT
499 self.create()
500
ca9b9221
MT
501 # Write everything to disk that is in the write queue
502 self.collecty.write_queue.commit_file(self.file)
503
b1ea4956
MT
504
505class GraphTemplate(object):
506 # A unique name to identify this graph template.
507 name = None
508
f181246a
MT
509 # Headline of the graph image
510 graph_title = None
511
512 # Vertical label of the graph
513 graph_vertical_label = None
514
515 # Limits
516 lower_limit = None
517 upper_limit = None
518
b1ea4956
MT
519 # Instructions how to create the graph.
520 rrd_graph = None
521
522 # Extra arguments passed to rrdgraph.
523 rrd_graph_args = []
524
c968f6d9
MT
525 # Default dimensions for this graph
526 height = GRAPH_DEFAULT_HEIGHT
527 width = GRAPH_DEFAULT_WIDTH
528
429ba506 529 def __init__(self, plugin, object_id, locale=None, timezone=None):
c968f6d9
MT
530 self.plugin = plugin
531
429ba506
MT
532 # Save localisation parameters
533 self.locale = locales.get(locale)
534 self.timezone = timezone
535
0308c0f3
MT
536 # Get all required RRD objects
537 self.object_id = object_id
538
539 # Get the main object
540 self.object = self.get_object(self.object_id)
541
c968f6d9
MT
542 def __repr__(self):
543 return "<%s>" % self.__class__.__name__
b1ea4956
MT
544
545 @property
546 def collecty(self):
c968f6d9 547 return self.plugin.collecty
b1ea4956 548
c968f6d9
MT
549 @property
550 def log(self):
551 return self.plugin.log
552
5913a52c 553 def _make_command_line(self, interval, format=DEFAULT_IMAGE_FORMAT,
edf268c7
MT
554 width=None, height=None, with_title=True, thumbnail=False):
555 args = [e for e in GRAPH_DEFAULT_ARGUMENTS]
c968f6d9 556
edf268c7
MT
557 # Set the default dimensions
558 default_height, default_width = GRAPH_DEFAULT_HEIGHT, GRAPH_DEFAULT_WIDTH
559
560 # A thumbnail doesn't have a legend and other labels
561 if thumbnail:
562 args.append("--only-graph")
563
564 default_height = THUMBNAIL_DEFAULT_HEIGHT
565 default_width = THUMBNAIL_DEFAULT_WIDTH
c968f6d9
MT
566
567 args += [
5913a52c 568 "--imgformat", format,
edf268c7
MT
569 "--height", "%s" % (height or default_height),
570 "--width", "%s" % (width or default_width),
73db5226 571 ]
eed405de 572
c968f6d9 573 args += self.rrd_graph_args
eed405de 574
f181246a 575 # Graph title
ca8a6cfa 576 if with_title and self.graph_title:
f181246a
MT
577 args += ["--title", self.graph_title]
578
579 # Vertical label
580 if self.graph_vertical_label:
581 args += ["--vertical-label", self.graph_vertical_label]
582
583 if self.lower_limit is not None or self.upper_limit is not None:
584 # Force to honour the set limits
585 args.append("--rigid")
586
587 if self.lower_limit is not None:
588 args += ["--lower-limit", self.lower_limit]
589
590 if self.upper_limit is not None:
591 args += ["--upper-limit", self.upper_limit]
592
c9351f4f 593 # Add interval
4d9ed86e 594 args += ["--start", util.make_interval(interval)]
c968f6d9
MT
595
596 return args
597
cd8bba0b
MT
598 def _add_vdefs(self, args):
599 ret = []
600
601 for arg in args:
602 ret.append(arg)
603
604 # Search for all DEFs and CDEFs
605 m = re.match(DEF_MATCH, "%s" % arg)
606 if m:
607 name = m.group(1)
608
609 # Add the VDEFs for minimum, maximum, etc. values
610 ret += [
611 "VDEF:%s_cur=%s,LAST" % (name, name),
612 "VDEF:%s_avg=%s,AVERAGE" % (name, name),
613 "VDEF:%s_max=%s,MAXIMUM" % (name, name),
614 "VDEF:%s_min=%s,MINIMUM" % (name, name),
615 ]
616
617 return ret
618
0308c0f3
MT
619 def get_object(self, *args, **kwargs):
620 return self.plugin.get_object(*args, **kwargs)
621
622 def get_object_table(self):
c968f6d9 623 return {
0308c0f3 624 "file" : self.object,
c968f6d9
MT
625 }
626
fc359f4d
MT
627 @property
628 def object_table(self):
629 if not hasattr(self, "_object_table"):
630 self._object_table = self.get_object_table()
631
632 return self._object_table
633
429ba506 634 def generate_graph(self, interval=None, **kwargs):
ca9b9221
MT
635 # Make sure that all collected data is in the database
636 # to get a recent graph image
637 if self.object:
638 self.object.commit()
639
c968f6d9
MT
640 args = self._make_command_line(interval, **kwargs)
641
642 self.log.info(_("Generating graph %s") % self)
c968f6d9 643
cd8bba0b
MT
644 if self.object:
645 args += self.object.make_rrd_defs()
eed405de 646
cd8bba0b
MT
647 args += self.rrd_graph
648 args = self._add_vdefs(args)
0308c0f3 649
699e99fb 650 # Convert arguments to string
5913a52c
MT
651 args = [str(e) for e in args]
652
cd8bba0b
MT
653 for arg in args:
654 self.log.debug(" %s" % arg)
655
429ba506 656 with Environment(self.timezone, self.locale.lang):
cb1ccb4f 657 graph = rrdtool.graphv("-", *args)
c968f6d9 658
a3864812
MT
659 return {
660 "image" : graph.get("image"),
661 "image_height" : graph.get("image_height"),
662 "image_width" : graph.get("image_width"),
663 }
664
665 def graph_info(self):
666 """
667 Returns a dictionary with useful information
668 about this graph.
669 """
670 return {
671 "title" : self.graph_title,
672 "object_id" : self.object_id or "",
673 "template" : self.name,
674 }