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