# HG changeset patch # User Tero Marttila # Date 1358714777 -7200 # Node ID 999ae3e9fdecc5e69c749c7360f0a8d10123836f # Parent 9f2967ba81ef9e39fc522e4dad22ed2d4784b1f3 pvl.web.rrd: split out pvl.rrd.args + pvl.rrd.rrds; bugfix diff -r 9f2967ba81ef -r 999ae3e9fdec bin/pvl.rrd-graph --- a/bin/pvl.rrd-graph Sun Jan 20 19:52:41 2013 +0200 +++ b/bin/pvl.rrd-graph Sun Jan 20 22:46:17 2013 +0200 @@ -7,6 +7,7 @@ __version__ = '0.1' import pvl.args +import pvl.rrd.args import pvl.rrd.graph import os.path @@ -33,13 +34,13 @@ # options parser.add_option_group(pvl.args.parser(parser)) + parser.add_option_group(pvl.rrd.args.parser(parser)) parser.add_option('--style', metavar='STYLE', default='detail', help="overview/detail") + parser.add_option('--interval', metavar='INTERVAL', default='daily', help="daily/weekly/yearly") - parser.add_option('--graph', metavar='PATH', default='.png', - help="output file") # parse options, args = parser.parse_args(argv[1:]) @@ -49,11 +50,12 @@ return options, args -def graph (options, rrd) : +def graph (options, rrds, rrd) : """ Graph given rrd. """ + # out path, ext = os.path.splitext(rrd) ext = options.graph @@ -70,9 +72,12 @@ """ options, args = parse_options(argv) + + # RRDDatabase + rrds = pvl.rrd.args.apply(options) for rrd in args : - graph(options, rrd) + graph(options, rrds, rrd) # done log.info("Exiting...") diff -r 9f2967ba81ef -r 999ae3e9fdec bin/pvl.verkko-rrd --- a/bin/pvl.verkko-rrd Sun Jan 20 19:52:41 2013 +0200 +++ b/bin/pvl.verkko-rrd Sun Jan 20 22:46:17 2013 +0200 @@ -8,6 +8,7 @@ from pvl import __version__ import pvl.args +import pvl.rrd.args import pvl.verkko.rrd import optparse @@ -31,15 +32,7 @@ # common parser.add_option_group(pvl.args.parser(parser)) - - parser.add_option('--rrd-type', metavar='TYPE', default='collectd', - help="mrtg/collectd") - - parser.add_option('--rrd', metavar='PATH', - help="Find RRD files") - - parser.add_option('--cache', metavar='PATH', - help="Cache RRD graphs") + parser.add_option_group(pvl.rrd.args.parser(parser)) # parse options, args = parser.parse_args(args) @@ -49,7 +42,6 @@ return options, args - def main (argv) : """ pvl.verkko wsgi development server. @@ -59,14 +51,8 @@ options, args = parse_argv(argv, doc=__doc__) # rrd - rrd_type = pvl.rrd.graph.interface(options.rrd_type) - - if not options.rrd : - log.error("no --rrd given") - return 2 - - rrd = pvl.verkko.rrd.RRDDatabase(rrd_type, options.rrd, options.cache) - + rrd = pvl.rrd.args.apply(options) + # app application = pvl.verkko.rrd.Application(rrd) diff -r 9f2967ba81ef -r 999ae3e9fdec pvl/rrd/__init__.py --- a/pvl/rrd/__init__.py Sun Jan 20 19:52:41 2013 +0200 +++ b/pvl/rrd/__init__.py Sun Jan 20 22:46:17 2013 +0200 @@ -4,6 +4,10 @@ Requires: python-rrdtool """ +from pvl.rrd.rrds import ( + RRDDatabase, + RRDCache, +) from pvl.rrd import ( api as rrd, diff -r 9f2967ba81ef -r 999ae3e9fdec pvl/rrd/args.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pvl/rrd/args.py Sun Jan 20 22:46:17 2013 +0200 @@ -0,0 +1,43 @@ +import optparse + +import pvl.rrd.graph # interface +from pvl.rrd import RRDDatabase, RRDCache + +def parser (parser) : + """ + optparse OptionGroup. + """ + + parser = optparse.OptionGroup(parser, "RRD options") + + parser.add_option('--rrd-type', metavar='TYPE', default='collectd', + help="mrtg/collectd") + + parser.add_option('--rrd', metavar='PATH', + help="Find RRD files") + + parser.add_option('--rrd-cache', metavar='PATH', + help="Cache RRD graphs") + + return parser + +def apply (options) : + """ + Return RRDDatabase from options. + """ + # path + if not options.rrd : + log.error("no --rrd given") + sys.exit(2) + + # type + graph_type = pvl.rrd.graph.interface_type(options.rrd_type) + + # cache + if options.rrd_cache : + cache = RRDCache(options.rrd_cache) + else : + cache = None + + return RRDDatabase(options.rrd, graph_type, cache) + diff -r 9f2967ba81ef -r 999ae3e9fdec pvl/rrd/graph.py --- a/pvl/rrd/graph.py Sun Jan 20 19:52:41 2013 +0200 +++ b/pvl/rrd/graph.py Sun Jan 20 22:46:17 2013 +0200 @@ -3,6 +3,8 @@ from pvl.invoke import merge # XXX +import logging; log = logging.getLogger('pvl.rrd.graph') + """ RRDTool graph builders. @@ -70,13 +72,7 @@ interval - interval(interval=...) """ - graph = cls() - graph = graph.common(title) - graph = graph.source(rrd) - graph = graph.style(style) - graph = graph.interval(interval) - - return graph + return cls().common(title).source(rrd).style(style).interval(interval) # builders def common (self, title) : @@ -124,6 +120,8 @@ if not (self.IN and self.OUT) : raise TypeError("No IN/OUT DS names for %s" % (self, )) + log.debug("%s", rrd) + return self( # data sources, bytes/s r'DEF:in0={rrd}:{ds_in}:AVERAGE'.format(rrd=rrd, ds_in=self.IN), @@ -282,7 +280,7 @@ 'collectd': CollectdIfOctets, } -def interface (type) : +def interface_type (type) : """ Lookup Interface -subclass for given type. """ diff -r 9f2967ba81ef -r 999ae3e9fdec pvl/rrd/rrds.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pvl/rrd/rrds.py Sun Jan 20 22:46:17 2013 +0200 @@ -0,0 +1,239 @@ +""" + RRD file on filesystem. +""" + +import os, os.path, errno + +import logging; log = logging.getLogger('pvl.rrd.rrds') + +class RRDDatabase (object) : + """ + A filesystem directory containing .rrd files. + """ + + def __init__ (self, path, graph_type, cache=None) : + """ + path - path to rrd dirs + graph_type - pvl.rrd.graph.InterfaceGraph type + cache - optional RRDCache + """ + + if not path : + raise ValueError("RRDDatabase: no path given") + + log.info("%s: graph_type=%s, cache=%s", path, graph_type, cache) + + self.graph_type = graph_type + self._path = path + self.cache = cache + + def path (self, *nodes) : + """ + Lookup and full filesystem path to the given relative RRD/dir path. + + Raises ValueError if invalid path. + """ + + # relative dir (no leading slash) -> absolute path + path = os.path.normpath(os.path.join(self._path, *nodes)) + + log.debug("%s: %s -> %s", self, nodes, path) + + # check inside base path + if not path.startswith(self._path.rstrip('/')) : + # mask + raise ValueError("%s: Invalid path: %s" % (self, nodes)) + + # ok + return path + + def tree (self, node=None) : + """ + Lookup and return XXX:RRDTree for given node, or root tree. + + Raises ValueError if invalid path, or no such tree. + """ + + # lookup fs path + if node : + path = self.path(node) + else : + path = self.path() + + # found? + if not os.path.isdir(path) : + raise ValueError("%s: Invalid tree: %s: %s" % (self, node, path)) + + if node : + return node + else : + return '' # equivalent + + 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 RRD(self, node, self.graph_type) + + def list (self, tree=None) : + """ + List (trees, rrds) under given tree. + """ + + dirs = [] + rrds = [] + + path = self.path(tree) + + for name in os.listdir(path) : + if name.startswith('.') : + continue + + path = self.path(tree, name) + + basename, extname = os.path.splitext(name) + + log.debug("%s: %s: %s: %s", self, tree, name, path) + + 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, cache=True) : + """ + Cached RRD.graph(), returning outfile. + + Uses self.cache if cache, otherwise graphs to tempfile. + """ + + path = rrd.path() + + if self.cache and cache : + # path in cache + out = self.cache.path(style, interval, str(rrd)) + + # hit/miss? + cache = self.cache.lookup(path, out) + + else : + # tempfile + out = None + cache = None + + log.debug("%s: %s: %s", self, rrd, out) + + if cache : + # from cache + outfile = open(out) + + else : + # to cache/tempfile + dimensions, lines, outfile = rrd.graph(style, interval).graph(out) + + return outfile + + def __str__ (self) : + return self._path + +class RRD (object) : + """ + An .rrd file on the filesystem, graphed using pvl.rrd.graph.Interface. + """ + + def __init__ (self, db, rrd, graph_type) : + self.db = db + self.rrd = rrd + self.graph_type = graph_type + + def path (self) : + return self.db.path(self.rrd) + '.rrd' + + def graph (self, style, interval) : + """ + Build Graph given rrd using given style/interval. + """ + + path = self.path() + + title = str(self) # " / ".join(rrd.split('/')) + return self.graph_type.build(title, path, style, interval) + + def __str__ (self) : + return self.rrd + +class RRDCache (object) : + """ + Cache graph output in the filesystem. + """ + + def __init__ (self, path) : + log.info("%s", path) + + self._path = path + + def path (self, *key) : + """ + Return cache path for key. + """ + + return os.path.join(self._path, *key) + '.png' + + 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 lookup (self, source, path) : + """ + Return hit from cache. + """ + + # 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 dst and src.st_mtime < dst.st_mtime : + log.debug("%s: %s: %s: hit", self, source, path) + + return True + + elif not dst: + log.debug("%s: %s: %s: miss", self, source, path) + return False + + else : + log.debug("%s: %s: %s: update", self, source, path) + return None + + def __str__ (self) : + return self._path diff -r 9f2967ba81ef -r 999ae3e9fdec pvl/verkko/rrd.py --- a/pvl/verkko/rrd.py Sun Jan 20 19:52:41 2013 +0200 +++ b/pvl/verkko/rrd.py Sun Jan 20 22:46:17 2013 +0200 @@ -3,7 +3,7 @@ http://verkko.paivola.fi/rrd """ -import pvl.web.application as web +from pvl import web from pvl.web import urls from pvl.web.html import tags as html @@ -11,186 +11,6 @@ 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 = ( @@ -211,7 +31,7 @@ tree = '' for part in _tree.split('/') : - tree = os.path.join(tree, part) + tree = urls.join(tree, part) yield part, self.url(Index, tree=tree) @@ -245,7 +65,7 @@ """ if self.tree : - path = os.path.join(self.tree, node) + path = urls.join(self.tree, node) else : path = node @@ -260,6 +80,7 @@ try : # XXX: unicode? self.tree = self.app.rrd.tree(tree) + except ValueError as ex : # mask raise web.NotFound(tree) @@ -368,7 +189,7 @@ self.rrd = self.app.rrd.rrd(target, self.tree) except ValueError as ex : - raise web.NotFound(tree, target) + raise web.NotFound(tree + '/' + target) self.style = style self.interval = interval @@ -401,6 +222,7 @@ # WSGI class Application (web.Application) : + # dispatch urls = urls.Map(( urls.rule('/', Index), urls.rule('/', Target), @@ -411,7 +233,7 @@ def __init__ (self, rrd) : """ - Initialize app with given RRDDatabase + rrd - pvl.rrd.RRDDatabase """ self.rrd = rrd diff -r 9f2967ba81ef -r 999ae3e9fdec pvl/web/__init__.py --- a/pvl/web/__init__.py Sun Jan 20 19:52:41 2013 +0200 +++ b/pvl/web/__init__.py Sun Jan 20 22:46:17 2013 +0200 @@ -6,6 +6,7 @@ from pvl.web.application import Application, Handler # werkzeug +from werkzeug.wrappers import Request, Response from werkzeug.exceptions import ( HTTPException, BadRequest, # 400 diff -r 9f2967ba81ef -r 999ae3e9fdec pvl/web/urls.py --- a/pvl/web/urls.py Sun Jan 20 19:52:41 2013 +0200 +++ b/pvl/web/urls.py Sun Jan 20 22:46:17 2013 +0200 @@ -1,5 +1,8 @@ from werkzeug.routing import Map, Rule +# url_join +from os.path import join + def rule (string, endpoint, **opts) : return Rule(string, endpoint=endpoint, defaults=opts)