# 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(" » ".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.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(" » ".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