pvl/verkko/rrd.py
changeset 152 33b98b46d8fb
child 153 8930f54b59b4
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/verkko/rrd.py	Sun Jan 20 18:26:54 2013 +0200
@@ -0,0 +1,355 @@
+# encoding: utf-8
+"""
+    http://verkko.paivola.fi/rrd
+"""
+
+import pvl.web.application as web
+from pvl.web import urls
+from pvl.web.html import tags as html
+
+import pvl.rrd
+
+import logging; log = logging.getLogger('pvl.verkko.rrd')
+
+# Model
+import os, os.path
+
+class RRDDatabase (object) :
+    """
+        A filesystem directory containing .rrd files.
+    """
+
+    def __init__ (self, path) :
+        log.info("%s", path)
+
+        self._path = path
+
+    def path (self, node=None, *subnodes) :
+        """
+            Lookup and full filesystem path to the given relative RRD/dir path.
+
+            Raises ValueError if invalid path.
+        """
+        
+        if node :
+            # relative dir (no leading slash) -> absolute path
+            path = os.path.normpath(os.path.join(self._path, node, *subnodes))
+        else :
+            path = self._path
+
+        log.debug("%s: %s -> %s", self, node, path)
+
+        # check inside base path
+        if not path.startswith(self._path) :
+            # mask
+            raise ValueError("%s: Invalid path: %s" % (self, node))
+
+        # ok
+        return path
+
+    def tree (self, node=None) :
+        """
+            Lookup and return RRDTree for given node, or root tree.
+
+            Raises ValueError if invalid path, or no such tree.
+        """
+
+        # lookup fs path
+        path = self.path(node)
+
+        # found?
+        if not os.path.isdir(path) :
+            raise ValueError("%s: Invalid tree: %s: %s" % (self, node, path))
+
+        return node
+
+    def rrd (self, node, tree=None) :
+        """
+            Lookup and return RRD for given node.
+        """
+
+        if tree :
+            node = os.path.join(tree, node)
+
+        node += '.rrd'
+        
+        path = self.path(node)
+
+        if not os.path.isfile(path) :
+            raise ValueError("%: Invalid rrd: %s: %s" % (self, node, path))
+
+        return node
+
+    def list (self, tree) :
+        """
+            List (trees, rrds) under given tree.
+        """
+
+        dirs = []
+        rrds = []
+        
+        for name in os.listdir(self.path(tree)) :
+            if name.startswith('.') :
+                continue
+
+            log.debug("%s: %s: %s", self, tree, name)
+            
+            path = self.path(tree, name)
+            basename, extname = os.path.splitext(name)
+
+            if os.path.isdir(path) :
+                dirs.append(name)
+
+            elif extname == '.rrd' :
+                # without the .rrd
+                rrds.append(basename)
+
+        # return sorted lists
+        return sorted(dirs), sorted(rrds)
+    
+    def graph (self, rrd, style, interval) :
+        """
+            Graph given rrd using given style/interval, returning the opened png data file.
+        """
+        
+        title = " / ".join(rrd.split('/'))
+
+        log.debug("%s: %s: %s/%s", self, rrd, style, interval)
+        
+        # XXX: lookup graph style..
+        # XXX: collectd
+        # XXX: out=None -> tempfile
+        dimensions, lines, outfile = pvl.rrd.graph.collectd_ifoctets(style, interval, title, self.path(rrd), None)
+
+        log.debug("%s: %s: %s", self, rrd, outfile)
+
+        return outfile
+
+    def __str__ (self) :
+        return str(self._path)
+
+# View/Controller
+class Handler (web.Handler) :
+    CSS = (
+        "/static/rrd.css", 
+    )
+
+    def title (self) :
+        return u"Päivölä Network - Network RRD Traffic Graphs"
+
+    def breadcrumb (self, _tree, target=None) :
+        """
+            Yield (title, url) navigation breadcrumbs
+        """
+
+        yield '/', self.url(Index)
+        
+        if _tree :
+            tree = ''
+            
+            for part in _tree.split('/') :
+                tree = os.path.join(tree, part)
+
+                yield part, self.url(Index, tree=tree)
+
+        if target :
+            # Target
+            yield target, self.url(Target, tree=tree, target=self.target)
+
+    def render_breadcrumb (self, tree, target=None) :
+        """
+            Render breadcrumb -> html.div
+        """
+
+        return html.div(id='breadcrumb')(html(" » ".join(
+            str(html.a(href=url)(node)) for node, url in self.breadcrumb(tree, target)))
+        )
+
+class Index (Handler) :
+    """
+        Browse trees, show overview graphs for targets.
+    """
+
+    def _title (self) :
+        if self.tree :
+            return html(" » ".join(self.tree.split('/')))
+        else :
+            return ""
+ 
+    def url_tree (self, node) :
+        """
+            Return url for given sub-node.
+        """
+        
+        if self.tree :
+            path = os.path.join(self.tree, node)
+        else :
+            path = node
+        
+        return self.url(tree=path)
+
+    def process (self, tree=None) :
+        """
+            Lookup path -> self.tree.
+        """
+
+        if tree :
+            try :
+                # XXX: unicode?
+                self.tree = self.app.rrd.tree(tree)
+            except ValueError as ex :
+                # mask
+                raise web.NotFound(tree)
+        else :
+            # root
+            self.tree = self.app.rrd.tree()
+
+    def render_list (self, items) :
+        return (
+            html.li(class_=('odd' if idx % 2 else 'even'))(item) for idx, item in enumerate(items)
+        )
+
+    def render_rrd (self, rrd) :
+        """
+            Render overview link/image for given rrd.
+        """
+
+        target_url = self.url(Target, tree=self.tree, target=rrd)
+        graph_url = self.url(Graph, tree=self.tree, target=rrd)
+        
+        return html.a(href=target_url)(
+                html.p(rrd),
+                html.img(src=graph_url),
+        )
+
+    def render (self) :
+        """
+            Render list of trees/rrds.
+        """
+
+        trees, rrds = self.app.rrd.list(self.tree)
+
+        return self.render_breadcrumb(self.tree), html.div(id='overview')(
+                html.ul(id='tree-list')(
+                    self.render_list(
+                        html.a(href=self.url_tree(subtree))(subtree)
+                    for subtree in trees)
+                ) if trees else None,
+
+                html.hr() if trees and rrds else None,
+
+                html.ul(id='rrd-list')(
+                    self.render_list(
+                        self.render_rrd(rrd)
+                    for rrd in rrds)
+                ) if rrds else None,
+        )
+
+class Target (Handler) :
+    """
+        Show graphs for RRD file.
+    """
+    
+    def _title (self) :
+        return html(" » ".join(self.rrd.split('/')))
+        
+    def process (self, target, tree=None) :
+        """
+            Lookup tree/target -> self.target
+        """
+        
+        try :
+            self.tree = self.app.rrd.tree(tree)
+            self.rrd = self.app.rrd.rrd(target, self.tree)
+            self.target = target
+
+        except ValueError as ex :
+            raise web.NotFound(tree, target)
+
+    def render_interval (self, interval, style='detail') :
+        """
+            Render detail link/image.
+        """
+
+        graph_url = self.url(Graph, tree=self.tree, target=self.target, style=style, interval=interval)
+        
+        return (
+                html.h2(interval.title()),
+                html.img(src=graph_url)
+        )
+
+    INTERVALS = ('daily', 'weekly', 'yearly')
+
+    def render (self) :
+        return self.render_breadcrumb(self.tree, self.target), html.div(id='detail')(
+                self.render_interval(interval) for interval in self.INTERVALS
+        )
+
+from pvl.invoke import merge # XXX
+import werkzeug # wrap_file
+
+class Graph (Handler) :
+    """
+        Render graph for RRD.
+    """
+
+    ARGS = { 'interval': 'daily', 'style': 'overview' }
+
+    def process (self, tree, target, style, interval) :
+        """
+            Return Graph for given options.
+        """
+
+        try :
+            self.tree = self.app.rrd.tree(tree)
+            self.rrd = self.app.rrd.rrd(target, self.tree)
+
+        except ValueError as ex :
+            raise web.NotFound(tree, target)
+        
+        self.style = style
+        self.interval = interval
+
+    def render_png (self) :
+        """
+            Return PNG data as a file-like object for our graph.
+        """
+
+        return self.app.rrd.graph(self.rrd, self.style, self.interval)
+
+    def respond (self) :
+        """
+            Return Response for our request.
+        """
+        
+        # process params+args -> self.graph
+        process = merge(self.params, dict((arg, self.request.args.get(arg, default)) for arg, default in self.ARGS.iteritems()))
+        response = self.process(**process)
+
+        if response :
+            return response
+        
+        # PNG output
+        render = self.render_png()
+        file = werkzeug.wrap_file(self.request.environ, render)
+        
+        # respond with file wrapper
+        return web.Response(file, mimetype='image/png', direct_passthrough=True)        
+
+# WSGI
+class Application (web.Application) :
+    urls = urls.Map((
+        urls.rule('/',                              Index),
+        urls.rule('/<target>',                      Target),
+        urls.rule('/<path:tree>/',                  Index),
+        urls.rule('/<path:tree>/<target>',          Target),
+        urls.rule('/<path:tree>/<target>.png',      Graph),
+    ))
+
+    def __init__ (self, rrd) :
+        """
+            Initialize app with given RRDDatabase
+        """
+
+        self.rrd = rrd
+