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 |