"""
Dynamic frontend
"""
import werkzeug
from werkzeug import exceptions
from werkzeug import Request, Response
from werkzeug.routing import Map, Rule
from rrdweb import html, graph
import os, os.path
import errno
import logging
# logging
log = logging.getLogger('rrdweb.wsgi')
class WSGIApp (object) :
def __init__ (self, rrdpath, tplpath, imgpath) :
"""
Configure
rrdpath - path to directory containing *.rrd files
tplpath - path to HTML templates
imgpath - path to generated PNG images. Must be writeable
"""
self.rrdpath = os.path.abspath(rrdpath)
self.templates = html.BaseFormatter(tplpath)
# XXX: some kind of fancy cache thingie :)
self.imgpath = os.path.abspath(imgpath)
# wrap to use werkzeug's Request/Response
@Request.application
def __call__ (self, req) :
"""
Main WSGI entry point
"""
try :
response = self.request(req)
except exceptions.HTTPException, e :
# format as response
return e.get_response(req.environ)
else :
# a-ok
return response
def request (self, req) :
"""
Wrapped request handler
"""
# map URLs against this request
urls = self.URLS.bind_to_environ(req)
# lookup URL against endpoint and dict of matched values from URL
endpoint, args = urls.match()
def build_url (method, **args) :
"""
Small wrapper around Werkzeug's routing.MapAdapter.build to suit our puroses
"""
return urls.build(method.im_func, args)
# invoke
# XXX: non-methods?
response = endpoint(self, req, build_url, **args)
return response
def scan_dir (self, dir) :
"""
Scan for RRD files and subdirectories directly underneath the given path.
Returns a ([subdir_name], [rrd_name]) tuple, with the sorted lists of subdirs and rrds.
"""
# we need to do this procedurally, because we collect two lists :(
subdirs = []
rrds = []
log.debug("Scanning dir %s", dir)
for name in os.listdir(dir) :
# skip hidden files
if name.startswith('.') :
continue
# path to file
path = os.path.join(dir, name)
# possible extesion
basename, extname = os.path.splitext(name)
log.debug("\tname=%s - %s", name, extname)
# collect subdirs
if os.path.isdir(path) :
subdirs.append(name)
# log.debug("\tsubdir: %s", name)
# collect .rrd's
elif extname == '.rrd' :
# without the .rrd
rrds.append(basename)
# log.debug("\trrd: %s", basename)
# return sorted lists
subdirs.sort()
rrds.sort()
return subdirs, rrds
def fmt_page (self, breadcrumb, content) :
"""
Render page with master layout as HTML.
breadcrumb - breadcrumb nav as HTML
content - main content as HTML
"""
log.debug("content = %r", content)
return self.templates.render('layout',
title = "MRTG", # XXX: some context
breadcrumb = breadcrumb,
content = content
)
def fmt_breadcrumb_segment (self, url, node, name) :
"""
Render the breadcrumb link for the given node as HTML.
"""
# real path
path = self.fs_path(node)
if os.path.isdir(path) :
# index
return dict(
url = url(self.index, dir=node),
name = name + '/',
)
else :
# XXX: assume .rrd
return dict(
url = url(self.target, rrd=node),
name = name,
)
def fmt_breadcrumb_segments (self, url, segments) :
"""
Render a sequence of segments for each of the nodes in the given list of segments.
"""
# root node
yield dict(
url = url(self.index),
name = "MRTG"
)
path = ''
for segment in segments :
# cumulative path
path = os.path.join(path, segment)
# format the induvidual node
yield self.fmt_breadcrumb_segment(url, path, segment)
def fmt_breadcrumb (self, url, node) :
"""
Render the breadcrumb for the given node's path as HTML.
"""
# split path to node into segments
segments = node.split('/')
log.debug("%r -> %r", node, segments)
# join segments together as hrefs
return " » ".join(
'<a href="%(url)s">%(name)s</a>' % html for html in self.fmt_breadcrumb_segments(url, segments)
)
def fmt_overview (self, url, dir, subdirs, rrds) :
"""
Render overview page listing given RRDs to HTML.
"""
if not dir :
dir = '/'
log.debug("Overview for %r with %d subdirs and %d rrds", dir, len(subdirs), len(rrds))
return self.templates.render('overview',
dir = dir,
overview_subdirs = '\n'.join(
self.fmt_overview_subdir(url, subdir, os.path.join(dir, subdir)) for subdir in subdirs
),
overview_graphs = '\n'.join(
self.fmt_overview_target(url, os.path.join(dir, rrd), idx) for idx, rrd in enumerate(rrds)
),
)
def fmt_overview_subdir (self, url, subdir, dir) :
"""
Render overview item for given subdir to HTML.
"""
return self.templates.render('overview-subdir',
dir_url = url(self.index, dir=dir),
dir_name = subdir,
)
def fmt_overview_target (self, url, rrd, idx) :
"""
Render overview item for given target to HTML.
"""
return self.templates.render('overview-target',
oddeven = 'odd' if idx % 2 else 'even',
title = self.rrd_title(rrd),
target_url = url(self.target, rrd=rrd),
daily_overview_img = url(self.graph, rrd=rrd, style='overview'),
)
def fmt_target (self, url, rrd) :
"""
Render target overview page to HTML.
"""
return self.templates.render('target',
title = self.rrd_title(rrd),
daily_img = url(self.graph, rrd=rrd, style='detail', interval='daily'),
weekly_img = url(self.graph, rrd=rrd, style='detail', interval='weekly'),
yearly_img = url(self.graph, rrd=rrd, style='detail', interval='yearly'),
)
def fs_path (self, node) :
"""
Lookup and return the full filesystem path to the given relative RRD/dir path.
"""
# dir is relative (no leading slash)
# full path
path = os.path.normpath(os.path.join(self.rrdpath, node))
# check inside base path
if not path.startswith(self.rrdpath) :
# mask
raise exceptions.NotFound(node)
# ok
return path
def rrd_path (self, rrd) :
"""
Lookup and return the full filesystem path to the given RRD name.
"""
# real path
path = self.fs_path(rrd + '.rrd')
# found as file?
if not os.path.isfile(path) :
raise exceptions.NotFound("No such RRD file: %s" % (rrd, ))
return path
def rrd_title (self, rrd) :
"""
Generate a neat human-readable title from the given RRD name.
"""
# XXX: path components...
return " » ".join(rrd.split('/'))
def render_graph (self, rrd, style, interval, png_path) :
"""
Render the given graph for the given RRD to the given path.
"""
rrd_path = self.rrd_path(rrd)
# title
# this is »
title = " / ".join(rrd.split('/'))
log.debug("%s -> %s", rrd_path, png_path)
# XXX: always generate
graph.mrtg(style, interval, title, rrd_path, png_path)
def rrd_graph (self, rrd, style, interval, flush=False) :
"""
Return an open file object representing the given graph's PNG image.
This is returned directly from cache, if a fresh copy is available. The cached copy is compared against
the source RRD file.
"""
# real path to .rrd
rrd_path = self.rrd_path(rrd)
# path to cached img
img_path = os.path.join(self.imgpath, style, interval, rrd) + '.png'
# this should always exist..
rrd_stat = os.stat(rrd_path)
try :
# this may not exist
img_stat = os.stat(img_path)
except OSError, e :
if e.errno == errno.ENOENT :
# doesn't exist
img_stat = None
else :
# can't handle
raise
# check freshness
if flush or img_stat is None or rrd_stat.st_mtime > img_stat.st_mtime :
# generate containing dir if missiong
dir_path = os.path.dirname(img_path)
if not os.path.isdir(dir_path) :
log.warn("makedirs %s", dir_path)
os.makedirs(dir_path)
# re-generate to tmp file
tmp_path = os.path.join(self.imgpath, style, interval, rrd) + '.tmp'
self.render_graph(rrd, style, interval, tmp_path)
# replace .png with .tmp (semi-atomic, but atomic enough..)
os.rename(tmp_path, img_path)
# open the now-fresh .png and return that
return open(img_path)
### Request handlers
# def node (self, url, path) :
# """
# Arbitrate between URLs to dirs and to RRDs.
# """
#
def index (self, req, url, dir = '') :
"""
Directory overview
dir - (optional) relative path to subdir from base rrdpath
"""
# lookup fs path
path = self.fs_path(dir)
# found?
if not os.path.isdir(path) :
raise exceptions.NotFound("No such RRD directory: %s" % (dir, ))
# scan
subdirs, rrds = self.scan_dir(path)
# render
html = self.fmt_page(
self.fmt_breadcrumb(url, dir),
self.fmt_overview(url, dir, subdirs, rrds)
)
return Response(html, mimetype='text/html')
def target (self, req, url, rrd) :
"""
Target overview
"""
# verify existance
path = self.rrd_path(rrd)
# render
html = self.fmt_page(
self.fmt_breadcrumb(url, rrd),
self.fmt_target(url, rrd)
)
return Response(html, mimetype='text/html')
STYLES = graph.STYLE_DEFS.keys()
INTERVALS = graph.INTERVAL_DEFS.keys()
def graph (self, req, url, rrd, style, interval) :
"""
Target graph
"""
# validate style/interval
if style not in self.STYLES or interval not in self.INTERVALS :
raise exceptions.BadRequest("Invalid style/interval")
# flush if asked to by ?flush
flush = ('flush' in req.args)
# render
png = self.rrd_graph(rrd, style, interval, flush=flush)
# construct wrapper for response file, using either werkzeug's own wrapper, or the one provided by the WSGI server
response_file = werkzeug.wrap_file(req.environ, png)
# respond with file wrapper
return Response(response_file, mimetype='image/png', direct_passthrough=True)
# map URLs to various methods
# XXX: this uses the method object as the endpoint, which is a bit silly, since it's not bound and we need to pass
# in self explicitly..
URLS = Map((
Rule('/', endpoint=index, defaults=dict(dir = '')),
Rule('/<path:dir>/', endpoint=index),
Rule('/<path:rrd>.rrd', endpoint=target),
Rule('/<path:rrd>.rrd/<string:style>.png', endpoint=graph, defaults=dict(interval = 'daily')),
Rule('/<path:rrd>.rrd/<string:style>/<string:interval>.png', endpoint=graph),
))