rrdweb/wsgi.py
changeset 32 47e977c23ba2
parent 24 29a523db66a8
equal deleted inserted replaced
31:cd9ca8068b09 32:47e977c23ba2
     5 import werkzeug
     5 import werkzeug
     6 from werkzeug import exceptions
     6 from werkzeug import exceptions
     7 from werkzeug import Request, Response
     7 from werkzeug import Request, Response
     8 from werkzeug.routing import Map, Rule
     8 from werkzeug.routing import Map, Rule
     9 
     9 
    10 from rrdweb import html, graph
    10 from rrdweb import html, graph, backend
    11 
    11 
    12 import os, os.path
    12 import os, os.path
    13 import errno
    13 import errno
    14 import logging
    14 import logging
    15 
    15 
    17 # logging
    17 # logging
    18 log = logging.getLogger('rrdweb.wsgi')
    18 log = logging.getLogger('rrdweb.wsgi')
    19 
    19 
    20 
    20 
    21 class WSGIApp (object) :
    21 class WSGIApp (object) :
    22     def __init__ (self, rrdpath, tplpath, imgpath) :
    22     def __init__ (self, rrdpath, tplpath, imgpath, rrdgraph=graph.pmacct_bytes) :
    23         """
    23         """
    24             Configure
    24             Configure
    25 
    25 
    26                 rrdpath         - path to directory containing *.rrd files
    26                 rrdpath         - path to directory containing *.rrd files
    27                 tplpath         - path to HTML templates
    27                 tplpath         - path to HTML templates
    28                 imgpath         - path to generated PNG images. Must be writeable
    28                 imgpath         - path to generated PNG images. Must be writeable
       
    29 
       
    30                 rrdgraph        - the graph.*_data function for rendering the graphs.
    29         """
    31         """
    30 
    32 
    31         self.rrdpath = os.path.abspath(rrdpath)
    33         self.rrdpath = os.path.abspath(rrdpath)
    32         self.templates = html.BaseFormatter(tplpath)
    34         self.templates = html.BaseFormatter(tplpath)
    33 
       
    34         # XXX: some kind of fancy cache thingie :)
       
    35         self.imgpath = os.path.abspath(imgpath)
    35         self.imgpath = os.path.abspath(imgpath)
       
    36 
       
    37         self.rrd_graph_func = rrdgraph
    36 
    38 
    37 
    39 
    38     # wrap to use werkzeug's Request/Response
    40     # wrap to use werkzeug's Request/Response
    39     @Request.application
    41     @Request.application
    40     def __call__ (self, req) :
    42     def __call__ (self, req) :
    77         # XXX: non-methods?
    79         # XXX: non-methods?
    78         response = endpoint(self, req, build_url, **args)
    80         response = endpoint(self, req, build_url, **args)
    79 
    81 
    80         return response
    82         return response
    81 
    83 
       
    84     def fs_path (self, name) :
       
    85         """
       
    86             Lookup and return the full filesystem path to the given relative RRD file/dir.
       
    87 
       
    88             The given name must be a relative path (no leading /).
       
    89 
       
    90             Raises NotFound for invalid paths.
       
    91         """
       
    92 
       
    93         # full path
       
    94         path = os.path.normpath(os.path.join(self.rrdpath, name))
       
    95 
       
    96         # check inside base path
       
    97         if not path.startswith(self.rrdpath) :
       
    98             # not found
       
    99             raise exceptions.NotFound(name)
       
   100 
       
   101         # ok
       
   102         return path
    82 
   103 
    83     def scan_dir (self, dir) :
   104     def scan_dir (self, dir) :
    84         """
   105         """
    85             Scan for RRD files and subdirectories directly underneath the given path.
   106             Scan for RRD files and subdirectories directly underneath the given path.
    86 
   107 
   251             Render target overview page to HTML.
   272             Render target overview page to HTML.
   252         """
   273         """
   253 
   274 
   254         return self.templates.render('target',
   275         return self.templates.render('target',
   255                 title               = self.rrd_title(rrd),
   276                 title               = self.rrd_title(rrd),
       
   277                 hourly_img          = url(self.graph, rrd=rrd, style='detail', interval='hourly'),
   256                 daily_img           = url(self.graph, rrd=rrd, style='detail', interval='daily'),
   278                 daily_img           = url(self.graph, rrd=rrd, style='detail', interval='daily'),
   257                 weekly_img          = url(self.graph, rrd=rrd, style='detail', interval='weekly'),
   279                 weekly_img          = url(self.graph, rrd=rrd, style='detail', interval='weekly'),
   258                 yearly_img          = url(self.graph, rrd=rrd, style='detail', interval='yearly'),
   280                 yearly_img          = url(self.graph, rrd=rrd, style='detail', interval='yearly'),
   259         )
   281         )
   260 
   282 
   261 
   283 
   262     def fs_path (self, node) :
       
   263         """
       
   264             Lookup and return the full filesystem path to the given relative RRD/dir path.
       
   265         """
       
   266 
       
   267         # dir is relative (no leading slash)
       
   268         # full path
       
   269         path = os.path.normpath(os.path.join(self.rrdpath, node))
       
   270 
       
   271         # check inside base path
       
   272         if not path.startswith(self.rrdpath) :
       
   273             # mask
       
   274             raise exceptions.NotFound(node)
       
   275 
       
   276         # ok
       
   277         return path
       
   278 
       
   279     def rrd_path (self, rrd) :
   284     def rrd_path (self, rrd) :
   280         """
   285         """
   281             Lookup and return the full filesystem path to the given RRD name.
   286             Lookup and return the full filesystem path to the given RRD name.
   282         """
   287         """
   283 
   288 
   298         # XXX: path components...
   303         # XXX: path components...
   299         return " » ".join(rrd.split('/'))
   304         return " » ".join(rrd.split('/'))
   300 
   305 
   301     def render_graph (self, rrd, style, interval, png_path) :
   306     def render_graph (self, rrd, style, interval, png_path) :
   302         """
   307         """
   303             Render the given graph for the given RRD to the given path.
   308             Render the given graph for the given RRD to the given path, returning the opened file object.
   304         """
   309         """
   305 
   310 
   306         rrd_path = self.rrd_path(rrd)
   311         rrd_path = self.rrd_path(rrd)
   307 
   312 
   308         # title
   313         # title
   309         # this is »
   314         # this is »
   310         title = " / ".join(rrd.split('/'))
   315         title = " / ".join(rrd.split('/'))
   311 
   316 
   312         log.debug("%s -> %s", rrd_path, png_path)
   317         log.debug("%s -> %s", rrd_path, png_path)
   313 
   318 
   314         # XXX: always generate
   319         # generate using the variant function given
   315         graph.mrtg(style, interval, title, rrd_path, png_path)
   320         w, h, report, png_file = graph.graph_single(style, interval, title, self.rrd_graph_func, rrd_path, png_path)
       
   321         
       
   322         # the open'd .tmp file
       
   323         return png_file
   316 
   324 
   317     def rrd_graph (self, rrd, style, interval, flush=False) :
   325     def rrd_graph (self, rrd, style, interval, flush=False) :
   318         """
   326         """
   319             Return an open file object representing the given graph's PNG image.
   327             Return an open file object representing the given graph's PNG image.
   320 
   328 
   354 
   362 
   355                 os.makedirs(dir_path)
   363                 os.makedirs(dir_path)
   356             
   364             
   357             # re-generate to tmp file
   365             # re-generate to tmp file
   358             tmp_path = os.path.join(self.imgpath, style, interval, rrd) + '.tmp'
   366             tmp_path = os.path.join(self.imgpath, style, interval, rrd) + '.tmp'
   359 
   367             
   360             self.render_graph(rrd, style, interval, tmp_path)
   368             # open and write the graph image
       
   369             img_file = self.render_graph(rrd, style, interval, tmp_path)
   361 
   370 
   362             # replace .png with .tmp (semi-atomic, but atomic enough..)
   371             # replace .png with .tmp (semi-atomic, but atomic enough..)
       
   372             # XXX: probably not portable to windows, what with img_file
   363             os.rename(tmp_path, img_path)
   373             os.rename(tmp_path, img_path)
   364 
   374         
   365         # open the now-fresh .png and return that
   375         else :
   366         return open(img_path)
   376             # use existing file
   367 
   377             img_file = open(img_path, 'rb')
   368 
   378         
       
   379         # return the now-fresh .png and return that
       
   380         return img_file
   369 
   381 
   370     ### Request handlers
   382     ### Request handlers
   371 
       
   372 
       
   373 #    def node (self, url, path) :
       
   374 #        """
       
   375 #            Arbitrate between URLs to dirs and to RRDs.
       
   376 #        """
       
   377 #
       
   378 
       
   379 
       
   380     def index (self, req, url, dir = '') :
   383     def index (self, req, url, dir = '') :
   381         """
   384         """
   382             Directory overview
   385             Directory overview
   383 
   386 
   384                 dir     - (optional) relative path to subdir from base rrdpath
   387                 dir     - (optional) relative path to subdir from base rrdpath
   385         """
   388         """
   386 
   389         
   387         # lookup fs path
   390         # lookup
   388         path = self.fs_path(dir)
   391         path = self.fs_path(dir)
   389 
       
   390         # found?
       
   391         if not os.path.isdir(path) :
       
   392             raise exceptions.NotFound("No such RRD directory: %s" % (dir, ))
       
   393 
   392 
   394         # scan
   393         # scan
   395         subdirs, rrds = self.scan_dir(path)
   394         subdirs, rrds = self.scan_dir(path)
   396         
   395         
   397         # render
   396         # render
   442         # construct wrapper for response file, using either werkzeug's own wrapper, or the one provided by the WSGI server
   441         # construct wrapper for response file, using either werkzeug's own wrapper, or the one provided by the WSGI server
   443         response_file = werkzeug.wrap_file(req.environ, png)
   442         response_file = werkzeug.wrap_file(req.environ, png)
   444         
   443         
   445         # respond with file wrapper
   444         # respond with file wrapper
   446         return Response(response_file, mimetype='image/png', direct_passthrough=True)        
   445         return Response(response_file, mimetype='image/png', direct_passthrough=True)        
   447 
   446     
       
   447     def graph_top (self, req, url, dir = '', count=5) :
       
   448         """
       
   449             Show top N hosts by peak/average.
       
   450         """
       
   451 
       
   452         # find
       
   453         path = self.fs_path(dir)
       
   454 
       
   455         # scan
       
   456         subdirs, rrds = self.scan_dir(path)
       
   457 
       
   458         # get top N hosts
       
   459         hosts = backend.calc_top_hosts(path, rrds, 'in', 'out', count=count)
       
   460         
       
   461         # draw graph with hosts
       
   462         w, h, data, img = graph.graph_multi('hourly', 
       
   463             "Top %d hosts" % count, 
       
   464             [('%s/%s.rrd' % (path, name), name) for name in hosts],
       
   465             self.rrd_graph_func,
       
   466             None   # to tmpfile
       
   467         )
       
   468         
       
   469         # wrap file output
       
   470         response_file = werkzeug.wrap_file(req.environ, img)
       
   471 
       
   472         return Response(response_file, mimetype='image/png', direct_passthrough=True)        
   448 
   473 
   449     # map URLs to various methods
   474     # map URLs to various methods
   450     # XXX: this uses the method object as the endpoint, which is a bit silly, since it's not bound and we need to pass
   475     # XXX: this uses the method object as the endpoint, which is a bit silly, since it's not bound and we need to pass
   451     #      in self explicitly..
   476     #      in self explicitly..
   452     URLS = Map((
   477     URLS = Map((
   453         Rule('/', endpoint=index, defaults=dict(dir = '')),
   478         Rule('/', endpoint=index, defaults=dict(dir = '')),
       
   479         Rule('/top.png', endpoint=graph_top, defaults=dict(dir = '')),
   454         Rule('/<path:dir>/', endpoint=index),
   480         Rule('/<path:dir>/', endpoint=index),
       
   481         Rule('/<path:dir>/top.png', endpoint=graph_top),
   455         Rule('/<path:rrd>.rrd', endpoint=target),
   482         Rule('/<path:rrd>.rrd', endpoint=target),
   456         Rule('/<path:rrd>.rrd/<string:style>.png', endpoint=graph, defaults=dict(interval = 'daily')),
   483         Rule('/<path:rrd>.rrd/<string:style>.png', endpoint=graph, defaults=dict(interval = 'daily')),
   457         Rule('/<path:rrd>.rrd/<string:style>/<string:interval>.png', endpoint=graph),
   484         Rule('/<path:rrd>.rrd/<string:style>/<string:interval>.png', endpoint=graph),
   458     ))
   485     ))
   459 
   486