import logging import os import pathlib import django.contrib.auth.models as amodels import rrdtool from django.conf import settings from django.db import models log = logging.getLogger(__name__) def _sanitize_path(path): # changing all separators to a _ should result in an ugly filename # that will not cause issues. # an additional _ is added at the beginning to prevent hidden files for sep in os.sep, os.altsep: if sep: path = path.replace(sep, "_") return "_" + path class ModelWithPerms(models.Model): users_read = models.ManyToManyField( amodels.User, related_name="%(class)s_read", blank=True, ) users_write = models.ManyToManyField( amodels.User, related_name="%(class)s_write", blank=True, ) groups_read = models.ManyToManyField( amodels.Group, related_name="%(class)s_read", blank=True, ) groups_write = models.ManyToManyField( amodels.Group, related_name="%(class)s_write", blank=True, ) class Meta: abstract = True class DataSource(ModelWithPerms): # an mqtt topic can be as long as 65,535 bytes when UTF-8 encoded, # which is probably too much for a sensible db topic = models.CharField(max_length=512) path = models.CharField( max_length=512, ) rrd_config = models.TextField( default=settings.RRD_DS_CONFIG ) active = models.BooleanField( default=True, ) def __str__(self): return self.topic @property def rrd_path(self): path = os.path.abspath(os.path.join( settings.RRD_DB_PATH, self.path )) base_path = os.path.abspath(settings.RRD_DB_PATH) if pathlib.Path(base_path) not in pathlib.Path(path).parents: return os.path.abspath(os.path.join( settings.RRD_DB_PATH, _sanitize_path(self.path) )) return path @property def ds_name(self): return self.topic.split("/")[-1] @property def lastupdate(self): try: last = rrdtool.lastupdate(self.rrd_path) except rrdtool.OperationalError as e: log.warning("Failure reading from ds: %s", e) return (None, None) else: return last["date"], last["ds"][self.ds_name] def update(self, ts, value): if not os.path.isfile(self.rrd_path): rrdtool.create( self.rrd_path, "--no-overwrite", self.rrd_config.format( ds_name=self.ds_name ).strip().split('\n'), ) try: rrdtool.update( self.rrd_path, str(ts) + ":" + str(value) ) except ValueError as e: log.warning("Could not update ds: %s", e) for graph in self.graph_set.all(): graph.update() class Graph(ModelWithPerms): title = models.CharField(max_length=64) data_sources = models.ManyToManyField(DataSource) path = models.CharField( max_length=512, ) rrd_config = models.TextField( default=settings.RRD_GRAPH_CONFIG ) def __str__(self): return self.title @property def graph_path(self): path = os.path.abspath(os.path.join( settings.RRD_GRAPH_PATH, self.path )) base_path = os.path.abspath(settings.RRD_GRAPH_PATH) if pathlib.Path(base_path) not in pathlib.Path(path).parents: return os.path.abspath(os.path.join( settings.RRD_GRAPH_PATH, _sanitize_path(self.path) )) return path def update(self): graph_path = self.graph_path os.makedirs(os.path.dirname(graph_path), exist_ok=True) rrd_paths = [] rrd_topics = [] rrd_ds_names = [] for ds in self.data_sources.all(): rrd_paths.append(ds.rrd_path) rrd_topics.append(ds.topic) rrd_ds_names.append(ds.ds_name) opts = self.rrd_config.format( topics=rrd_topics, ds_names=rrd_ds_names, ds_paths=rrd_paths, title=self.title, ).strip().split('\n') opts = [o.strip() for o in opts] rrdtool.graph( graph_path, * opts ) class Dashboard(ModelWithPerms): title = models.CharField(max_length=64) graphs = models.ManyToManyField(Graph) data_sources = models.ManyToManyField(DataSource) template = models.TextField() def __str__(self): return self.title