--- /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
+