pvl/verkko/rrd.py
author Tero Marttila <terom@paivola.fi>
Sun, 20 Jan 2013 19:52:41 +0200
changeset 155 9f2967ba81ef
parent 154 11df86fd2d67
child 156 999ae3e9fdec
permissions -rw-r--r--
pvl.rrd.graph: refactor to use Graph -> Interface -> Mrtg/CollectdIfOctets
# 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, errno

class RRDDatabase (object) :
    """
        A filesystem directory containing .rrd files.
    """

    def __init__ (self, graph, path, cache=None) :
        """
            graph   - pvl.rrd.graph.InterfaceGraph type
            path    - path to rrd dirs
            cache   - path to cache dirs
        """

        if not path :
            raise ValueError("RRDDatabase: no path given")

        log.info("%s: type=%s, cache=%s", path, graph, cache)
        
        self._graph = graph
        self._path = path
        self._cache = cache

    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)
        
        path = self.path(node) + '.rrd'

        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 _stat (self, path) :
        """
            os.stat or None.
        """

        try :
            return os.stat(path)

        except OSError as ex :
            if ex.errno == errno.ENOENT :
                return None
            else :
                raise

    def cache (self, source, *key) :
        """
            Lookup given key from cache, returning (hit, file).
        """

        # output
        if not self._cache :
            return None, None

        # cache path
        path = os.path.join(self._cache, *key)
        
        # create
        dir = os.path.dirname(path)
        if not os.path.isdir(dir) :
            log.warn("makedirs %s", dir)
            os.makedirs(dir)
        
        # stats's
        src = self._stat(source)
        dst = self._stat(path)

        if not dst:
            log.debug("%s: %s: %s: miss", self._cache, source, path)
            return None, path
            
        elif dst and src.st_mtime < dst.st_mtime :
            log.debug("%s: %s: %s: hit", self._cache, source, path)

            return True, path

        else :
            log.debug("%s: %s: %s: update", self._cache, source, path)
            return False, path

    def graph (self, rrd, style, interval) :
        """
            Graph given rrd using given style/interval, returning the opened png data file.
        """
        
        title = str(rrd) # " / ".join(rrd.split('/'))
        
        path = self.path(rrd) + '.rrd'

        cached, out = self.cache(path, style, interval, rrd + '.png')
        
        log.debug("%s: %s: %s", self, rrd, out)
        
        if cached :
            # from cache
            outfile = open(out)

        else :
            # to cache
            dimensions, lines, outfile = self._graph.build(title, path, style, interval).graph(out)

        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(" &raquo; ".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(" &raquo; ".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.h3(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(" &raquo; ".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