--- a/rrdweb/html.py Tue Nov 02 00:27:46 2010 +0200
+++ b/rrdweb/html.py Tue Nov 02 04:11:06 2010 +0200
@@ -5,37 +5,60 @@
import os.path
-class Formatter (object) :
+class BaseFormatter (object) :
+ """
+ Trivial HTML template formatter.
+ """
+
+ def __init__ (self, basedir, encoding = 'utf-8') :
+ """
+ basedir - directory containing the required .html files
+ encoding - unicode encoding of file content (default: utf-8)
+ """
+
+ self.basedir = os.path.abspath(basedir)
+ self.encoding = encoding
+
+ def render (self, name, **vars) :
+ """
+ Format and return given template.
+
+ name - basename of template without .html
+ **vars - template context
+
+ The template is rendered and returned as unicode.
+ """
+
+ path = os.path.join(self.basedir, name) + '.html'
+
+ # read contents
+ data = open(path).read().decode(self.encoding)
+
+ # format
+ return data % vars
+
+
+# XXX: legacy
+class Formatter (BaseFormatter) :
+
TEMPLATE_DIR = 'etc/templates'
IMG_URL = '%(prefix)s?t=%(target)s&s=%(style)&i=%(interval)s'
TARGET_URL = '%(prefix)s?t=%(target)s'
def __init__ (self, template_dir=TEMPLATE_DIR, url_prefix='', img_url=IMG_URL, target_url=TARGET_URL) :
- self.template_dir = template_dir
+ BaseFormatter.__init__(self, template_dir, 'utf-8')
+
self.url_prefix = url_prefix
self.img_url = img_url
self.target_url = target_url
- def tpl (self, name, **vars) :
- """
- Format and return given template
- """
-
- path = os.path.join(self.template_dir, name) + '.html'
-
- # read contents
- data = open(path).read()
-
- # format
- return data % vars
-
def page (self, content) :
"""
Format page contents
"""
- return self.tpl('layout', content=content)
+ return self.render('layout', content=content)
def fmt_img_url (self, style, interval, target) :
return self.img_url % dict(
@@ -56,9 +79,9 @@
Format target listing
"""
- return self.page(self.tpl('overview',
+ return self.page(self.render('overview',
overview_graphs = '\n'.join(
- self.tpl('overview-target',
+ self.render('overview-target',
title = target.title,
daily_overview_img = self.fmt_img_url('overview', 'daily', target),
target_url = self.fmt_target_url(target),
@@ -72,7 +95,7 @@
Format a specific target
"""
- return self.page(self.tpl('target',
+ return self.page(self.render('target',
title = target.title,
daily_img = self.fmt_img_url('detail', 'daily', target),
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/rrdweb/wsgi.py Tue Nov 02 04:11:06 2010 +0200
@@ -0,0 +1,459 @@
+"""
+ 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)) for rrd in 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) :
+ """
+ Render overview item for given target to HTML.
+ """
+
+ return self.templates.render('overview-target',
+ 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),
+ ))
+
+