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: