# HG changeset patch # User Tero Marttila # Date 1358699214 -7200 # Node ID 33b98b46d8fbf9c270a440f6581632f1c22cc71e # Parent 8a9f01036091f8bc2ce306f842942c480f2a781f pvl.verkko.rrd: reimplementation of old rrdweb using pvl.web/rrd diff -r 8a9f01036091 -r 33b98b46d8fb bin/pvl.verkko-rrd --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/pvl.verkko-rrd Sun Jan 20 18:26:54 2013 +0200 @@ -0,0 +1,74 @@ +#!/usr/bin/python + +""" + pvl.verkko.rrd wsgi development server +""" + +import werkzeug.serving + +from pvl import __version__ +import pvl.args +import pvl.verkko.rrd + +import optparse +import logging; log = logging.getLogger('main') + +def parse_argv (argv, doc = __doc__) : + """ + Parse command-line argv, returning (options, args). + """ + + prog = argv.pop(0) + args = argv + + # optparse + parser = optparse.OptionParser( + prog = prog, + usage = '%prog: [options] [ [...]]', + version = __version__, + description = doc, + ) + + # common + parser.add_option_group(pvl.args.parser(parser)) + + parser.add_option('--rrd', metavar='PATH', + help="Path to RRD files") + + # parse + options, args = parser.parse_args(args) + + # apply + pvl.args.apply(options) + + return options, args + +def main (argv) : + """ + pvl.verkko wsgi development server. + """ + + # parse cmdline + options, args = parse_argv(argv, doc=__doc__) + + # rrd + rrd = pvl.verkko.rrd.RRDDatabase(options.rrd) + + # app + application = pvl.verkko.rrd.Application(rrd) + + # wsgi wrapper + werkzeug.serving.run_simple('0.0.0.0', 8080, application, + #use_reloader = True, + use_debugger = (options.loglevel == logging.DEBUG), + static_files = { + '/static': 'static', + }, + ) + +if __name__ == '__main__' : + import sys + + sys.exit(main(sys.argv)) + + diff -r 8a9f01036091 -r 33b98b46d8fb pvl/verkko/rrd.py --- /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), + urls.rule('//', Index), + urls.rule('//', Target), + urls.rule('//.png', Graph), + )) + + def __init__ (self, rrd) : + """ + Initialize app with given RRDDatabase + """ + + self.rrd = rrd + diff -r 8a9f01036091 -r 33b98b46d8fb static/rrd.css --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/static/rrd.css Sun Jan 20 18:26:54 2013 +0200 @@ -0,0 +1,60 @@ +/* Nagivation breadcrumb */ +div#breadcrumb +{ + padding: 5pt; + + border-bottom: 1px dotted #aaa; +} + +div#breadcrumb a +{ + color: #444; + font-weight: bold; + text-decoration: none; +} + +div#breadcrumb a:hover +{ + text-decoration: underline; +} + +/* Directory overview */ +#overview ul +{ + list-style-type: none; +} + +#overview ul li +{ + margin: 5pt; + padding: 5pt; +} + +#overview ul li.even +{ + background-color: #fff; +} + +#overview ul li.odd +{ + background-color: #eee; +} + +#overview ul li div.title +{ + padding: 5pt; +} + +#overview ul a +{ + color: #444; + font-weight: bold; + font-size: large; + text-decoration: none; +} + +#overview ul a:hover +{ + text-decoration: underline; +} +