pvl.rrd: copy+update from old rrdweb
authorTero Marttila <terom@paivola.fi>
Sun, 20 Jan 2013 15:37:59 +0200
changeset 143 fb48ba17ae3e
parent 142 e5dafbc87cbb
child 144 9966d35a63df
pvl.rrd: copy+update from old rrdweb
bin/pvl.rrd-graph
pvl/rrd/__init__.py
pvl/rrd/api.py
pvl/rrd/graph.py
--- /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))
--- /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
+"""
+
--- /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))
+
--- /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)
+