]> git.ipfire.org Git - collecty.git/blobdiff - src/collecty/plugins/base.py
Refectoring of the main classes
[collecty.git] / src / collecty / plugins / base.py
index cf9c3b402410bf085fd7b0e825644aef42d6fba4..5b130444324f5c530efdad3b97e3155888d35c4e 100644 (file)
@@ -23,15 +23,20 @@ import datetime
 import logging
 import math
 import os
+import re
 import rrdtool
 import tempfile
 import threading
 import time
 import unicodedata
 
+from .. import locales
+from .. import util
 from ..constants import *
 from ..i18n import _
 
+DEF_MATCH = re.compile(r"C?DEF:([A-Za-z0-9_]+)=")
+
 class Timer(object):
        def __init__(self, timeout, heartbeat=1):
                self.timeout = timeout
@@ -64,6 +69,41 @@ class Timer(object):
                return self.elapsed > self.timeout
 
 
+class Environment(object):
+       """
+               Sets the correct environment for rrdtool to create
+               localised graphs and graphs in the correct timezone.
+       """
+       def __init__(self, timezone, locale):
+               # Build the new environment
+               self.new_environment = {
+                       "TZ" : timezone or DEFAULT_TIMEZONE,
+               }
+
+               for k in ("LANG", "LC_ALL"):
+                       self.new_environment[k] = locale or DEFAULT_LOCALE
+
+       def __enter__(self):
+               # Save the current environment
+               self.old_environment = {}
+               for k in self.new_environment:
+                       self.old_environment[k] = os.environ.get(k, None)
+
+               # Apply the new one
+               os.environ.update(self.new_environment)
+
+       def __exit__(self, type, value, traceback):
+               # Roll back to the previous environment
+               for k, v in self.old_environment.items():
+                       if v is None:
+                               try:
+                                       del os.environ[k]
+                               except KeyError:
+                                       pass
+                       else:
+                               os.environ[k] = v
+
+
 class PluginRegistration(type):
        plugins = {}
 
@@ -195,15 +235,17 @@ class Plugin(object, metaclass=PluginRegistration):
 
                        return object
 
-       def get_template(self, template_name, object_id):
+       def get_template(self, template_name, object_id, locale=None, timezone=None):
                for template in self.templates:
                        if not template.name == template_name:
                                continue
 
-                       return template(self, object_id)
+                       return template(self, object_id, locale=locale, timezone=timezone)
 
-       def generate_graph(self, template_name, object_id="default", **kwargs):
-               template = self.get_template(template_name, object_id=object_id)
+       def generate_graph(self, template_name, object_id="default",
+                       timezone=None, locale=None, **kwargs):
+               template = self.get_template(template_name, object_id=object_id,
+                       timezone=timezone, locale=locale)
                if not template:
                        raise RuntimeError("Could not find template %s" % template_name)
 
@@ -217,6 +259,22 @@ class Plugin(object, metaclass=PluginRegistration):
 
                return graph
 
+       def graph_info(self, template_name, object_id="default",
+                       timezone=None, locale=None, **kwargs):
+               template = self.get_template(template_name, object_id=object_id,
+                       timezone=timezone, locale=locale)
+               if not template:
+                       raise RuntimeError("Could not find template %s" % template_name)
+
+               return template.graph_info()
+
+       def last_update(self, object_id="default"):
+               object = self.get_object(object_id)
+               if not object:
+                       raise RuntimeError("Could not find object %s" % object_id)
+
+               return object.last_update()
+
 
 class Object(object):
        # The schema of the RRD database.
@@ -245,6 +303,9 @@ class Object(object):
        def __repr__(self):
                return "<%s>" % self.__class__.__name__
 
+       def __lt__(self, other):
+               return self.id < other.id
+
        @property
        def collecty(self):
                return self.plugin.collecty
@@ -312,6 +373,39 @@ class Object(object):
        def info(self):
                return rrdtool.info(self.file)
 
+       def last_update(self):
+               """
+                       Returns a dictionary with the timestamp and
+                       data set of the last database update.
+               """
+               return {
+                       "dataset"   : self.last_dataset,
+                       "timestamp" : self.last_updated,
+               }
+
+       def _last_update(self):
+               return rrdtool.lastupdate(self.file)
+
+       @property
+       def last_updated(self):
+               """
+                       Returns the timestamp when this database was last updated
+               """
+               lu = self._last_update()
+
+               if lu:
+                       return lu.get("date")
+
+       @property
+       def last_dataset(self):
+               """
+                       Returns the latest dataset in the database
+               """
+               lu = self._last_update()
+
+               if lu:
+                       return lu.get("ds")
+
        @property
        def stepsize(self):
                return self.plugin.interval
@@ -350,6 +444,46 @@ class Object(object):
 
                return schema
 
+       @property
+       def rrd_schema_names(self):
+               ret = []
+
+               for line in self.rrd_schema:
+                       (prefix, name, type, lower_limit, upper_limit) = line.split(":")
+                       ret.append(name)
+
+               return ret
+
+       def make_rrd_defs(self, prefix=None):
+               defs = []
+
+               for name in self.rrd_schema_names:
+                       if prefix:
+                               p = "%s_%s" % (prefix, name)
+                       else:
+                               p = name
+
+                       defs += [
+                               "DEF:%s=%s:%s:AVERAGE" % (p, self.file, name),
+                       ]
+
+               return defs
+
+       def get_stddev(self, interval=None):
+               args = self.make_rrd_defs()
+
+               # Add the correct interval
+               args += ["--start", util.make_interval(interval)]
+
+               for name in self.rrd_schema_names:
+                       args += [
+                               "VDEF:%s_stddev=%s,STDEV" % (name, name),
+                               "PRINT:%s_stddev:%%lf" % name,
+                       ]
+
+               x, y, vals = rrdtool.graph("/dev/null", *args)
+               return dict(zip(self.rrd_schema_names, vals))
+
        def execute(self):
                if self.collected:
                        raise RuntimeError("This object has already collected its data")
@@ -367,6 +501,9 @@ class Object(object):
                # Make sure that the RRD database has been created
                self.create()
 
+               # Write everything to disk that is in the write queue
+               self.collecty.write_queue.commit_file(self.file)
+
 
 class GraphTemplate(object):
        # A unique name to identify this graph template.
@@ -388,27 +525,23 @@ class GraphTemplate(object):
        # Extra arguments passed to rrdgraph.
        rrd_graph_args = []
 
-       intervals = {
-               None   : "-3h",
-               "hour" : "-1h",
-               "day"  : "-25h",
-               "month": "-30d",
-               "week" : "-360h",
-               "year" : "-365d",
-       }
-
        # Default dimensions for this graph
        height = GRAPH_DEFAULT_HEIGHT
        width  = GRAPH_DEFAULT_WIDTH
 
-       def __init__(self, plugin, object_id):
+       def __init__(self, plugin, object_id, locale=None, timezone=None):
                self.plugin = plugin
 
+               # Save localisation parameters
+               self.locale = locales.get(locale)
+               self.timezone = timezone
+
                # Get all required RRD objects
                self.object_id = object_id
 
                # Get the main object
-               self.object = self.get_object(self.object_id)
+               self.objects = self.get_objects(self.object_id)
+               self.objects.sort()
 
        def __repr__(self):
                return "<%s>" % self.__class__.__name__
@@ -421,22 +554,38 @@ class GraphTemplate(object):
        def log(self):
                return self.plugin.log
 
+       @property
+       def object(self):
+               """
+                       Shortcut to the main object
+               """
+               if len(self.objects) == 1:
+                       return self.objects[0]
+
        def _make_command_line(self, interval, format=DEFAULT_IMAGE_FORMAT,
-                       width=None, height=None):
-               args = []
+                       width=None, height=None, with_title=True, thumbnail=False):
+               args = [e for e in GRAPH_DEFAULT_ARGUMENTS]
+
+               # Set the default dimensions
+               default_height, default_width = GRAPH_DEFAULT_HEIGHT, GRAPH_DEFAULT_WIDTH
+
+               # A thumbnail doesn't have a legend and other labels
+               if thumbnail:
+                       args.append("--only-graph")
 
-               args += GRAPH_DEFAULT_ARGUMENTS
+                       default_height = THUMBNAIL_DEFAULT_HEIGHT
+                       default_width = THUMBNAIL_DEFAULT_WIDTH
 
                args += [
                        "--imgformat", format,
-                       "--height", "%s" % (height or self.height),
-                       "--width", "%s" % (width or self.width),
+                       "--height", "%s" % (height or default_height),
+                       "--width", "%s" % (width or default_width),
                ]
 
                args += self.rrd_graph_args
 
                # Graph title
-               if self.graph_title:
+               if with_title and self.graph_title:
                        args += ["--title", self.graph_title]
 
                # Vertical label
@@ -453,58 +602,95 @@ class GraphTemplate(object):
                        if self.upper_limit is not None:
                                args += ["--upper-limit", self.upper_limit]
 
-               try:
-                       interval = self.intervals[interval]
-               except KeyError:
-                       interval = "end-%s" % interval
-
                # Add interval
-               args += ["--start", interval]
+               args += ["--start", util.make_interval(interval)]
 
                return args
 
-       def get_object(self, *args, **kwargs):
-               return self.plugin.get_object(*args, **kwargs)
+       def _add_defs(self):
+               use_prefix = len(self.objects) >= 2
 
-       def get_object_table(self):
-               return {
-                       "file" : self.object,
-               }
+               args = []
+               for object in self.objects:
+                       if use_prefix:
+                               args += object.make_rrd_defs(object.id)
+                       else:
+                               args += object.make_rrd_defs()
 
-       @property
-       def object_table(self):
-               if not hasattr(self, "_object_table"):
-                       self._object_table = self.get_object_table()
+               return args
+
+       def _add_vdefs(self, args):
+               ret = []
+
+               for arg in args:
+                       ret.append(arg)
+
+                       # Search for all DEFs and CDEFs
+                       m = re.match(DEF_MATCH, "%s" % arg)
+                       if m:
+                               name = m.group(1)
 
-               return self._object_table
+                               # Add the VDEFs for minimum, maximum, etc. values
+                               ret += [
+                                       "VDEF:%s_cur=%s,LAST" % (name, name),
+                                       "VDEF:%s_avg=%s,AVERAGE" % (name, name),
+                                       "VDEF:%s_max=%s,MAXIMUM" % (name, name),
+                                       "VDEF:%s_min=%s,MINIMUM" % (name, name),
+                               ]
 
-       def get_object_files(self):
-               files = {}
+               return ret
 
-               for id, obj in self.object_table.items():
-                       files[id] = obj.file
+       def get_objects(self, *args, **kwargs):
+               object = self.plugin.get_object(*args, **kwargs)
 
-               return files
+               if object:
+                       return [object,]
+
+               return []
 
        def generate_graph(self, interval=None, **kwargs):
+               assert self.objects, "Cannot render graph without any objects"
+
+               # Make sure that all collected data is in the database
+               # to get a recent graph image
+               for object in self.objects:
+                       object.commit()
+
                args = self._make_command_line(interval, **kwargs)
 
                self.log.info(_("Generating graph %s") % self)
-               self.log.debug("  args: %s" % args)
 
-               object_files = self.get_object_files()
+               rrd_graph = self.rrd_graph
 
-               for item in self.rrd_graph:
-                       try:
-                               args.append(item % object_files)
-                       except TypeError:
-                               args.append(item)
+               # Add DEFs for all objects
+               if not any((e.startswith("DEF:") for e in rrd_graph)):
+                       args += self._add_defs()
 
-                       self.log.debug("  %s" % args[-1])
+               args += rrd_graph
+               args = self._add_vdefs(args)
 
                # Convert arguments to string
                args = [str(e) for e in args]
 
-               graph = rrdtool.graphv("-", *args)
+               for arg in args:
+                       self.log.debug("  %s" % arg)
+
+               with Environment(self.timezone, self.locale.lang):
+                       graph = rrdtool.graphv("-", *args)
+
+               return {
+                       "image"        : graph.get("image"),
+                       "image_height" : graph.get("image_height"),
+                       "image_width"  : graph.get("image_width"),
+               }
 
-               return graph.get("image")
+       def graph_info(self):
+               """
+                       Returns a dictionary with useful information
+                       about this graph.
+               """
+               return {
+                       "title"        : self.graph_title,
+                       "object_id"    : self.object_id or "",
+                       "template"     : self.name,
+               }