# HG changeset patch # User Tero Marttila # Date 1358689079 -7200 # Node ID fb48ba17ae3e3672e04d58e7d47b12fc980dd67a # Parent e5dafbc87cbb641b0c9dcdf14075ea51740f336e pvl.rrd: copy+update from old rrdweb diff -r e5dafbc87cbb -r fb48ba17ae3e bin/pvl.rrd-graph --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/bin/pvl.rrd-graph Sun Jan 20 15:37:59 2013 +0200 @@ -0,0 +1,84 @@ +#!/usr/bin/env python + +""" + pvl.rrd graph output +""" + +__version__ = '0.1' + +import pvl.args +import pvl.rrd.graph + +import os.path + +import logging, optparse + +log = logging.getLogger('main') + +def parse_options (argv) : + """ + Parse command-line arguments. + """ + + prog = argv[0] + + parser = optparse.OptionParser( + prog = prog, + usage = '%prog: [options]', + version = __version__, + + # module docstring + description = __doc__, + ) + + # options + parser.add_option_group(pvl.args.parser(parser)) + + parser.add_option('--style', metavar='STYLE', default='detail', + help="overview/detail") + parser.add_option('--interval', metavar='INTERVAL', default='daily', + help="daily/weekly/yearly") + parser.add_option('--graph', metavar='PATH', default='.png', + help="output file") + + # parse + options, args = parser.parse_args(argv[1:]) + + # apply + pvl.args.apply(options, prog) + + return options, args + +def graph (options, rrd) : + """ + Graph given rrd. + """ + + # out + path, ext = os.path.splitext(rrd) + ext = options.graph + out = path + ext + + # graph + log.info("%s -> %s", rrd, out) + + pvl.rrd.graph.collectd_ifoctets(options.style, options.interval, "Test", rrd, out) + +def main (argv) : + """ + Usage: [options] rrd + """ + + options, args = parse_options(argv) + + for rrd in args : + graph(options, rrd) + + # done + log.info("Exiting...") + return 0 + +if __name__ == '__main__': + import sys + + sys.exit(main(sys.argv)) diff -r e5dafbc87cbb -r fb48ba17ae3e pvl/rrd/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pvl/rrd/__init__.py Sun Jan 20 15:37:59 2013 +0200 @@ -0,0 +1,7 @@ +""" + RRD Graphing + + Requires: + python-rrdtool +""" + diff -r e5dafbc87cbb -r fb48ba17ae3e pvl/rrd/api.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pvl/rrd/api.py Sun Jan 20 15:37:59 2013 +0200 @@ -0,0 +1,111 @@ +import rrdtool + +import pvl.invoke + +import logging; log = logging.getLogger('pvl.rrd.api') + +""" + Wrapper around the rrdtool python interface +""" + +def timestamp (time=None) : + """ + Format datetime value for rrdtool. + """ + + if not time : + return None + + elif isinstance(time, datetime.datetime) : + return int(time.mktime(dt.timetuple())) + + elif isinstance(time, datetime.timedelta) : + raise NotImplementedError("pvl.rrd.api.timestamp: timedelta") + + else : + # dunno + return str(dt) + +def cmd (func, pre, opts, post) : + """ + Run the given rrdtool.* function, formatting the given positional arguments and options. + + Returns the return value, which varies... + """ + + log.debug("%s: %s: %s: %s", func, pre, opts, post) + + # { opt: arg } -> [ '--opt', arg ] + opts = pvl.invoke.optargs(**opts) + + # positional arguments + pre = pvl.invoke.optargs(*pre) + post = pvl.invoke.optargs(*post) + + return func(*(pre + opts + post)) + +def graph (out=None, *args, **opts) : + """ + Render a graph image and/or print a report from data stored in one or several RRDs. + + out - None -> tempfile + - False -> stdout + - path -> write to file + + Returns: + (width, height) - pixel dimensions of the resulting graph image + report_output - any PRINT'd output (?) + graph_file - file-like object containing graph image, unless out=False -> stdout + + With out=None, the returned graph_file is a tempfile which will be cleaned up by Python once close()'d! + """ + + if out is None : + # tempfile + out_file = tempfile.NamedTemporaryFile(suffix='.png', delete=True) # python2.6 + out_path = out.name + + elif out is False : + out_file = None + out_path = '-' + + else : + # for reading + out_path = out + out_file = True # open later + + # XXX: handle tempfile close? + width, height, out_lines = cmd(rrdtool.graph, (out_path, ), opts, args) + + if out_file is True : + out_file = open(out_path) + + return (width, height), out_lines, out_file + +def fetch (rrd, cf, **opts) : + """ + Fetch values from RRD. + + Returns + (start, end, step) - xrange(...) for row timestamps + (ds, ...) - columns (ds name) + ((value, ...), ...) - rows (data by ds) + """ + + return cmd(rrdtool.fetch, (rrd, cf), opts, ()) + +def _fetch (rrd, cf='AVERAGE', resolution=None, start=None, end=None, **opts) : + """ + Yields (timestamp, { ds: value }) for given ds-values, or all. + """ + + steps, sources, rows = fetch(rrd, cf, + resolution = resolution, + start = timestamp(start), + end = timestamp(end), + **opts + ) + + for ts, row in zip(xrange(*steps), rows) : + yield datetime.fromtimestamp(ts), dict(zip(sources, row)) + diff -r e5dafbc87cbb -r fb48ba17ae3e pvl/rrd/graph.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/pvl/rrd/graph.py Sun Jan 20 15:37:59 2013 +0200 @@ -0,0 +1,209 @@ +import pvl.rrd.api as rrd +import time + +from pvl.invoke import merge # XXX + +""" + RRDTool graph output for MRTG, or collectd if_octets +""" + +def timestamp () : + return time.strftime("%Y/%m/%d %H:%M:%S %Z") + +def common_opts () : + """ + Common options for all views + """ + + return dict( + # output + imgformat = "PNG", + # lazy = True, + + color = [ + # disable border + # border = 0, + "SHADEA#ffffff00", + "SHADEB#ffffff00", + + # keep background transparent + "BACK#ffffff00", + "SHADEB#ffffff00", + ], + + # labels + vertical_label = "bits/s", + units = "si", + + # use logarithmic scaling? + # logarithmic = True, + + # smooth out lines + slope_mode = True, + ) + +def overview_opts () : + """ + Common options for the overview graph + """ + + return dict( + width = 600, + height = 50, + ), [ + "CDEF:all=in,out,+", + + "VDEF:max=all,MAXIMUM", + "VDEF:avg=all,AVERAGE", + "VDEF:min=all,MINIMUM", + + "LINE1:in#0000FF:In", + "LINE1:out#00CC00:Out", + + "GPRINT:max:%6.2lf %Sbps max", + "GPRINT:avg:%6.2lf %Sbps avg", + "GPRINT:min:%6.2lf %Sbps min\\l", + ] + +def detail_opts () : + """ + Common options for the detail graph + """ + + return dict( + # dimensions + width = 600, + height = 200, + ), [ + # values + 'VDEF:in_max=in,MAXIMUM', + 'VDEF:in_avg=in,AVERAGE', + 'VDEF:in_min=in,MINIMUM', + 'VDEF:in_cur=in,LAST', + 'VDEF:out_max=out,MAXIMUM', + 'VDEF:out_avg=out,AVERAGE', + 'VDEF:out_min=out,MINIMUM', + 'VDEF:out_cur=out,LAST', + + # legend/graph + "COMMENT:%4s" % "", + "COMMENT:%11s" % "Maximum", + "COMMENT:%11s" % "Average", + "COMMENT:%11s" % "Minimum", + "COMMENT:%11s\\l" % "Current", + + "LINE1:in#0000FF:%4s" % "In", + 'GPRINT:in_max:%6.2lf %Sbps', + 'GPRINT:in_avg:%6.2lf %Sbps', + 'GPRINT:in_min:%6.2lf %Sbps', + 'GPRINT:in_cur:%6.2lf %Sbps\\l', + + "LINE1:out#00CC00:%4s" % "Out", + 'GPRINT:out_max:%6.2lf %Sbps', + 'GPRINT:out_avg:%6.2lf %Sbps', + 'GPRINT:out_min:%6.2lf %Sbps', + 'GPRINT:out_cur:%6.2lf %Sbps\\l', + + # mark + "COMMENT:Generated %s\\r" % timestamp().replace(':', '\\:'), + ] + +STYLE_DEFS = { + 'overview': overview_opts, + 'detail': detail_opts, +} + +def daily_opts (title) : + """ + Common options for the 'daily' view + """ + + return dict( + # labels + x_grid = "MINUTE:15:HOUR:1:HOUR:4:0:%H:%M", + + # general info + title = "Daily %s" % (title, ), + + # interval + start = "-24h", + ) + +def weekly_opts (title) : + return dict( + # labels + # x_grid = "MINUTE:15:HOUR:1:HOUR:4:0:%H:%M", + + # general info + title = "Weekly %s" % (title, ), + + # interval + start = "-7d", + ) + +def yearly_opts (title) : + return dict( + # labels + # x_grid = "MINUTE:15:HOUR:1:HOUR:4:0:%H:%M", + + # general info + title = "Yearly %s" % (title, ), + + # interval + start = "-1y", + ) + + +INTERVAL_DEFS = { + 'daily': daily_opts, + 'weekly': weekly_opts, + 'yearly': yearly_opts, +} + +def mrtg_data (rrd_path) : + """ + Data sources for network in/out from an MRTG rrd + """ + + return [ + # data sources, bytes/s + r'DEF:in0=%s:ds0:AVERAGE' % rrd_path, + r'DEF:out0=%s:ds1:AVERAGE' % rrd_path, + + # data, bits/s + 'CDEF:in=in0,8,*', + 'CDEF:out=out0,8,*', + + ] + +def collectd_data (rrd_path) : + """ + Data sources for if_octets from a collectd rrd + """ + + return [ + # data sources, bytes/s + r'DEF:in0=%s:rx:AVERAGE' % rrd_path, + r'DEF:out0=%s:tx:AVERAGE' % rrd_path, + + # data, bits/s + 'CDEF:in=in0,8,*', + 'CDEF:out=out0,8,*', + + ] + +def _graph (style, interval, title, data_func, rrd_path, out_path) : + style_opts, style_vars = STYLE_DEFS[style]() + interval_opts = INTERVAL_DEFS[interval](title) + + opts = merge(common_opts(), style_opts, interval_opts) + data = data_func(rrd_path) + style_vars + + return rrd.graph(out_path, *data, **opts) + +def mrtg (style, interval, title, rrd_path, out_path) : + return _graph(style, interval, title, mrtg_data, rrd_path, out_path) + +def collectd_ifoctets (style, interval, title, rrd_path, out_path) : + return _graph(style, interval, title, collectd_data, rrd_path, out_path) +