-#!/usr/bin/python
+#!/usr/bin/python3
###############################################################################
# #
# collecty - A system statistics collection daemon for IPFire #
# #
###############################################################################
-from __future__ import division
-
import datetime
import logging
import math
import tempfile
import threading
import time
+import unicodedata
+from .. import locales
from ..constants import *
from ..i18n import _
-_plugins = {}
-
-def get():
- """
- Returns a list with all automatically registered plugins.
- """
- return _plugins.values()
-
class Timer(object):
def __init__(self, timeout, heartbeat=1):
self.timeout = timeout
return self.elapsed > self.timeout
-class Plugin(threading.Thread):
+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 = {}
+
+ def __init__(plugin, name, bases, dict):
+ type.__init__(plugin, name, bases, dict)
+
+ # The main class from which is inherited is not registered
+ # as a plugin.
+ if name == "Plugin":
+ return
+
+ if not all((plugin.name, plugin.description)):
+ raise RuntimeError(_("Plugin is not properly configured: %s") % plugin)
+
+ PluginRegistration.plugins[plugin.name] = plugin
+
+
+def get():
+ """
+ Returns a list with all automatically registered plugins.
+ """
+ return PluginRegistration.plugins.values()
+
+class Plugin(object, metaclass=PluginRegistration):
# The name of this plugin.
name = None
# The default interval for all plugins
interval = 60
- # Automatically register all providers.
- class __metaclass__(type):
- def __init__(plugin, name, bases, dict):
- type.__init__(plugin, name, bases, dict)
-
- # The main class from which is inherited is not registered
- # as a plugin.
- if name == "Plugin":
- return
-
- if not all((plugin.name, plugin.description)):
- raise RuntimeError(_("Plugin is not properly configured: %s") \
- % plugin)
-
- _plugins[plugin.name] = plugin
-
def __init__(self, collecty, **kwargs):
- threading.Thread.__init__(self, name=self.description)
- self.daemon = True
-
self.collecty = collecty
# Check if this plugin was configured correctly.
# Run some custom initialization.
self.init(**kwargs)
- # Keepalive options
- self.running = True
- self.timer = Timer(self.interval)
-
self.log.debug(_("Successfully initialized %s") % self.__class__.__name__)
@property
try:
result = o.collect()
- if isinstance(result, tuple) or isinstance(result, list):
- result = ":".join(("%s" % e for e in result))
+ result = self._format_result(result)
except:
self.log.warning(_("Unhandled exception in %s.collect()") % o, exc_info=True)
continue
self.collecty.write_queue.add(o, now, result)
# Returns the time this function took to complete.
- return (time.time() - time_start)
+ delay = time.time() - time_start
- def run(self):
- self.log.debug(_("%s plugin has started") % self.name)
+ # Log some warning when a collect method takes too long to return some data
+ if delay >= 60:
+ self.log.warning(_("A worker thread was stalled for %.4fs") % delay)
- # Initially collect everything
- self.collect()
+ @staticmethod
+ def _format_result(result):
+ if not isinstance(result, tuple) and not isinstance(result, list):
+ return result
- while self.running:
- # Reset the timer.
- self.timer.reset()
+ # Replace all Nones by NaN
+ s = []
- # Wait until the timer has successfully elapsed.
- if self.timer.wait():
- delay = self.collect()
- self.timer.reset(delay)
+ for e in result:
+ if e is None:
+ e = "NaN"
- self.log.debug(_("%s plugin has stopped") % self.name)
+ # Format as string
+ e = "%s" % e
- def shutdown(self):
- self.log.debug(_("Received shutdown signal."))
- self.running = False
+ s.append(e)
- # Kill any running timers.
- if self.timer:
- self.timer.cancel()
+ return ":".join(s)
def get_object(self, id):
for object in self.objects:
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)
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.
rrd_schema = None
# RRA properties.
- rra_types = ["AVERAGE", "MIN", "MAX"]
- rra_timespans = [3600, 86400, 604800, 2678400, 31622400]
- rra_rows = 2880
+ rra_types = ("AVERAGE", "MIN", "MAX")
+ rra_timespans = (
+ ("1m", "10d"),
+ ("1h", "18M"),
+ ("1d", "5y"),
+ )
def __init__(self, plugin, *args, **kwargs):
self.plugin = plugin
"""
The absolute path to the RRD file of this plugin.
"""
- return os.path.join(DATABASE_DIR, self.plugin.path, "%s.rrd" % self.id)
+ filename = self._normalise_filename("%s.rrd" % self.id)
+
+ return os.path.join(DATABASE_DIR, self.plugin.path, filename)
+
+ @staticmethod
+ def _normalise_filename(filename):
+ # Convert the filename into ASCII characters only
+ filename = unicodedata.normalize("NFKC", filename)
+
+ # Replace any spaces by dashes
+ filename = filename.replace(" ", "-")
+
+ return filename
### Basic methods
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
xff = 0.1
- cdp_length = 0
- for rra_timespan in self.rra_timespans:
- if (rra_timespan / self.stepsize) < self.rra_rows:
- rra_timespan = self.stepsize * self.rra_rows
-
- if cdp_length == 0:
- cdp_length = 1
- else:
- cdp_length = rra_timespan // (self.rra_rows * self.stepsize)
-
- cdp_number = math.ceil(rra_timespan / (cdp_length * self.stepsize))
-
- for rra_type in self.rra_types:
- schema.append("RRA:%s:%.10f:%d:%d" % \
- (rra_type, xff, cdp_length, cdp_number))
+ for steps, rows in self.rra_timespans:
+ for type in self.rra_types:
+ schema.append("RRA:%s:%s:%s:%s" % (type, xff, steps, rows))
return schema
None : "-3h",
"hour" : "-1h",
"day" : "-25h",
+ "month": "-30d",
"week" : "-360h",
"year" : "-365d",
}
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
if self.upper_limit is not None:
args += ["--upper-limit", self.upper_limit]
- # Add interval
- args.append("--start")
-
try:
- args.append(self.intervals[interval])
+ interval = self.intervals[interval]
except KeyError:
- args.append(str(interval))
+ interval = "end-%s" % interval
+
+ # Add interval
+ args += ["--start", interval]
return args
"file" : self.object,
}
+ @property
+ def object_table(self):
+ if not hasattr(self, "_object_table"):
+ self._object_table = self.get_object_table()
+
+ return self._object_table
+
def get_object_files(self):
files = {}
- for id, obj in self.get_object_table().items():
+ for id, obj in self.object_table.items():
files[id] = obj.file
return files
self.log.debug(" %s" % args[-1])
- return self.write_graph(*args)
-
- def write_graph(self, *args):
- # Convert all arguments to string
+ # Convert arguments to string
args = [str(e) for e in args]
- with tempfile.NamedTemporaryFile() as f:
- rrdtool.graph(f.name, *args)
+ with Environment(self.timezone, self.locale.lang):
+ graph = rrdtool.graphv("-", *args)
- # Get back to the beginning of the file
- f.seek(0)
+ return {
+ "image" : graph.get("image"),
+ "image_height" : graph.get("image_height"),
+ "image_width" : graph.get("image_width"),
+ }
- # Return all the content
- return f.read()
+ 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,
+ }