Observium_CE/scripts/rrdtoolx.py

177 lines
6.1 KiB
Python
Executable File

#!/usr/bin/python
"""rrdtool extended commands.
Usage:
%(cmd)s summary <filename>
%(cmd)s addrra <filename> <outfile> [RRA:CF:cf args] ...
Adds "summary" and "addrra" rrdtool commands. The "summary" command
will output a short summary of the RRD's DSs and RRAs. The "addrra"
command will create a new rrd with added RRAs. These RRAs will be
populated with as much data as can be derived by rrdxport from the
existing RRAs.
"""
__author__ = "Donovan Baarda <abo@minkirri.apana.org.au>"
__license__ = "LGPL"
__version__ = "$Revision: b358159c47eb $"
__date__ = "$Date: 2013/09/24 11:14:56 $"
__url__ = "http://minkirri.apana.org.au/~abo/projects/rrdcollect/"
__requires__ = "rrdtool"
import os
import re
import sys
import subprocess
import tempfile
# Note we don't bother using the python rrdtool module because it doesn't
# support dump, restore, or xport.
def rrdtool(cmd):
"""Run rrdtool and return output."""
# We set LC_NUMERIC=C to ensure eval() parses numbers right.
return subprocess.check_output('LC_ALL=; LC_NUMERIC=C; rrdtool ' + cmd, shell=True)
class DS(object):
"""A simple DS object."""
def __init__(self, ds=None):
if ds:
_, self.name, self.type, heartbeat, ds_min, ds_max = ':'.split(ds)
self.minimal_heartbeat = int(heartbeat)
self.min = ds_min is 'U' and None or int(ds_min)
self.max = ds_max is 'U' and None or int(ds_max)
def __str__(self):
ds_min = self.min is None and 'U' or self.min
ds_max = self.max is None and 'U' or self.max
return 'DS:%s:%s:%s:%s:%s' % (self.name, self.type, self.minimal_heartbeat, ds_min, ds_max)
def __repr__(self):
return '%s.%s("%s")' % (self.__module__, self.__class__.__name__, self)
class RRA(object):
"""A simple RRA object"""
def __init__(self, rra=None):
if rra:
_, self.cf, xff, steps, rows = rra.split(':')
self.xff = float(xff)
self.pdp_per_row = int(steps)
self.rows = int(rows)
def __str__(self):
return 'RRA:%s:%s:%s:%s' % (self.cf, self.xff, self.pdp_per_row, self.rows)
def __repr__(self):
return '%s.%s("%s")' % (self.__module__, self.__class__.__name__, self)
class RRD(object):
"""A simple RRD object"""
INFO_RE = re.compile(r'^(.*?)\[(.*?)\]\.(.*)$')
def __init__(self, rrdpath):
"""Initiallise an RRD by parsing its info."""
info = rrdtool('info %s' % rrdpath)
ds={}
rra={}
for k,v in (l.split(' = ') for l in info.splitlines()):
# Evaluate values and use None for NaN.
try:
v = eval(v)
except NameError:
v = None
try:
kind, key, field = self.INFO_RE.match(k).groups()
if kind == 'ds':
setattr(ds.setdefault(key, DS()), field, v)
elif kind == 'rra':
setattr(rra.setdefault(int(key), RRA()), field, v)
except AttributeError:
setattr(self, k, v)
# Set ds.name attribute from dict keys.
for k,v in ds.items(): v.name = k
# Turn ds into a dict keyed by index.
ds = dict((d.index,d) for d in ds.values())
# Set self.ds and self.rra as lists in index order.
self.ds = [ds[i] for i in sorted(ds)]
self.rra = [rra[i] for i in sorted(rra)]
def getSummary(self):
"""Returns a string summary of the RRD."""
return '%s step=%s last_update=%s\n%s\n%s\n' % (
rrd.filename, rrd.step, rrd.last_update,
'\n'.join(str(d) for d in rrd.ds),
'\n'.join(str(d) for d in rrd.rra))
def _getRRADataXml(self, rra):
"""Get the xml data for an added RRA from existing RRAs."""
step = rra.pdp_per_row * self.step
end = self.last_update / step * step
start = end - rra.rows * step
defs = ' '.join('DEF:%s=%s:%s:%s' % (ds.name, self.filename, ds.name, rra.cf) for ds in self.ds)
xports = ' '.join('XPORT:%s' % ds.name for ds in self.ds)
cmd = 'xport -s %s -e %s -m %s --step %s %s %s' % (
start, end, rra.rows, step, defs, xports)
xml = rrdtool(cmd)
# Get stuff between <data> tags and drop the last past end time row.
data_xml = re.search(r'<data>\n(.*)^.+?^\s*</data>', xml, flags=re.M|re.S).group(1)
# turn it into rrdtool dump database format.
data_xml = re.sub(r'^\s*<row><t>(.*)</t>', r'\t\t\t<!-- \1 --><row>', data_xml, flags=re.M)
# Put in <database> tags.
data_xml = '\t\t<database>\n%s\t\t</database>' % data_xml
return data_xml
def _getRRAXml(self, rra):
"""Get the xml for an added RRA."""
# Get the xml for a new rrd with the existing DSs and the new RRA.
filename = tempfile.mkstemp('rrd')[1]
dss = ' '.join(str(ds) for ds in self.ds)
rrdtool('create %s -b 0 -s %s %s %s' % (filename, self.step, dss, rra))
xml = rrdtool('dump %s' % filename)
os.unlink(filename)
# Get the new RRA definition between the <rra> tags.
rra_xml = re.search(r'^\s*<rra>.*</rra>\n', xml, flags=re.M|re.S).group(0)
# If possible replace <database> with data generated from existing RRAs.
if rra.cf in (r.cf for r in self.rra):
database_xml = self._getRRADataXml(rra)
rra_xml = re.sub(r'^\s*<database>.*</database>', database_xml, rra_xml, flags=re.M|re.S)
return rra_xml
def _getAddRRAXml(self, rras):
"""Get the xml for an rrd with added RRAs."""
xml = rrdtool('dump %s' % rrd.filename)
for rra in rras:
# Generate and append the new RRAs.
rra_xml = self._getRRAXml(rra)
xml = re.sub(r'(^</rrd>)', r'%s\1' % rra_xml, xml, flags=re.M|re.S)
return xml
def addRRA(self, filename, rras):
"""Create a new rrd file with the added RRAs."""
xml = self._getAddRRAXml(rras)
xmlfile = tempfile.mkstemp('rrd.xml')[1]
open(xmlfile, 'w+b').write(xml)
rrdtool('restore %s %s' % (xmlfile, filename))
os.unlink(xmlfile)
if __name__ == "__main__":
if len(sys.argv)<3 or sys.argv[1] in ("-?", "-h", "help"):
print __doc__ % dict(cmd=os.path.basename(sys.argv[0]))
sys.exit(1)
cmd = sys.argv[1]
rrd=RRD(sys.argv[2])
if cmd == "addrra":
filename = sys.argv[3]
rras = [RRA(r) for r in sys.argv[4:]]
rrd.addRRA(filename, rras)
elif cmd == "summary":
print rrd.getSummary()
else:
sys.stderr.write('Error: Unknown command %r.\n' % cmd)
sys.exit(1)