aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--rcynic/rcynic.py551
1 files changed, 272 insertions, 279 deletions
diff --git a/rcynic/rcynic.py b/rcynic/rcynic.py
index 8155129d..c3a86ac2 100644
--- a/rcynic/rcynic.py
+++ b/rcynic/rcynic.py
@@ -32,6 +32,8 @@ try:
except ImportError:
from xml.etree.ElementTree import (ElementTree, Element, SubElement, Comment)
+session = None
+
opt = {
"refresh" : 1800,
"suppress-zero-columns" : True,
@@ -39,6 +41,7 @@ opt = {
"show-detailed-status" : True,
"show-problems" : False,
"show-summary" : True,
+ "show-timestamps" : True,
"show-graphs" : False,
"suppress-backup-whining" : True,
"one-file-per-section" : False,
@@ -49,92 +52,83 @@ def usage(msg = 0):
f.write("Usage: %s %s [options] [input_file [output_file_or_directory]]\n" % (sys.executable, sys.argv[0]))
f.write("Options:\n")
for i in sorted(opt):
- if not isinstance(opt[i], bool):
+ if "_" not in i and not isinstance(opt[i], bool):
f.write(" --%-30s (%s)\n" % (i + " <value>", opt[i]))
for i in sorted(opt):
- if isinstance(opt[i], bool):
+ if "_" not in i and isinstance(opt[i], bool):
f.write(" --[no-]%-25s (--%s%s)\n" % (i, "" if opt[i] else "no-", i))
if msg:
f.write("\n")
sys.exit(msg)
-opts = ["help"]
-for i in opt:
- if isinstance(opt[i], bool):
- opts.append(i)
- opts.append("no-" + i)
- else:
- opts.append(i + "=")
+def parse_options():
-try:
- opts, argv = getopt.getopt(sys.argv[1:], "h?", opts)
- for o, a in opts:
- if o in ("-?", "-h", "--help"):
- usage(0)
- negated = o.startswith("--no-")
- o = o[5:] if negated else o[2:]
- if isinstance(opt[o], bool):
- opt[o] = not negated
- elif isinstance(opt[o], int):
- opt[o] = int(a)
+ opts = ["help"]
+ for i in opt:
+ if isinstance(opt[i], bool):
+ opts.append(i)
+ opts.append("no-" + i)
else:
- opt[o] = a
-except Exception, e:
- usage("%s: %s" % (e.__class__.__name__, str(e)))
-
-input_file = argv[0] if len(argv) > 0 else None
-output_foo = argv[1] if len(argv) > 1 else None
-
-if len(argv) > 2:
- usage("Unexpected arguments")
-
-output_directory = output_file = None
-
-if opt["one-file-per-section"] or opt["show-graphs"]:
- output_directory = output_foo
- if output_directory is None:
- usage("--show-graphs and --one-file-per-section require an output directory")
- if not os.path.isdir(output_directory):
- os.makedirs(output_directory)
-else:
- output_file = output_foo
-
-del output_foo
+ opts.append(i + "=")
+
+ try:
+ opts, argv = getopt.getopt(sys.argv[1:], "h?", opts)
+ for o, a in opts:
+ if o in ("-?", "-h", "--help"):
+ usage(0)
+ negated = o.startswith("--no-")
+ o = o[5:] if negated else o[2:]
+ if isinstance(opt[o], bool):
+ opt[o] = not negated
+ elif isinstance(opt[o], int):
+ opt[o] = int(a)
+ else:
+ opt[o] = a
+ except Exception, e:
+ usage("%s: %s" % (e.__class__.__name__, str(e)))
+
+ opt["input_file"] = argv[0] if len(argv) > 0 and argv[0] != "-" else None
+ output_foo = argv[1] if len(argv) > 1 and argv[1] != "-" else None
+
+ if len(argv) > 2:
+ usage("Unexpected arguments")
+
+ opt["output_directory"] = opt["output_file"] = None
+
+ if opt["one-file-per-section"] or opt["show-graphs"]:
+ opt["output_directory"] = output_foo
+ else:
+ opt["output_file"] = output_foo
-html = None
-body = None
+ if opt["one-file-per-section"] or opt["show-graphs"]:
+ if opt["output_directory"] is None:
+ usage("--show-graphs and --one-file-per-section require an output directory")
+ if not os.path.isdir(opt["output_directory"]):
+ os.makedirs(opt["output_directory"])
def parse_utc(s):
return int(time.mktime(time.strptime(s, "%Y-%m-%dT%H:%M:%SZ")))
class Label(object):
- def __init__(self, elt, pos):
+ def __init__(self, elt):
self.code = elt.tag
self.mood = elt.get("kind")
self.text = elt.text.strip()
- self.pos = pos
- self.sum = 0
+ self.count = 0
- def __cmp__(self, other):
- return cmp(self.pos, other.pos)
+ def get_count(self):
+ return self.count
class Validation_Status(object):
- @classmethod
- def set_label_map(cls, labels):
- cls.label_map = dict((l.code, l) for l in labels)
-
- def __init__(self, elt):
+ def __init__(self, elt, label_map):
self.uri = elt.text.strip()
self.timestamp = elt.get("timestamp")
self.generation = elt.get("generation")
self.hostname = urlparse.urlparse(self.uri).hostname or None
self.fn2 = os.path.splitext(self.uri)[1] or None if self.generation else None
- self.label = self.label_map[elt.get("status")]
-
- def stand_up_and_be_counted(self):
- self.label.sum += 1
+ self.label = label_map[elt.get("status")]
@property
def code(self):
@@ -170,6 +164,7 @@ class Host(object):
self.uris = set()
self.graph = None
self.counters = {}
+ self.totals = {}
def add_connection(self, elt):
self.elapsed += parse_utc(elt.get("finished")) - parse_utc(elt.get("started"))
@@ -180,14 +175,15 @@ class Host(object):
def add_validation_status(self, v):
if v.generation == "current":
self.uris.add(v.uri)
- self.counters[(v.fn2, v.generation, v.code)] = self.get_counter(v.fn2, v.generation, v.code) + 1
- self.counters[v.code] = self.get_total(v.code) + 1
+ self.counters[(v.fn2, v.generation, v.label)] = self.get_counter(v.fn2, v.generation, v.label) + 1
+ self.totals[v.label] = self.get_total(v.label) + 1
+ v.label.count += 1
- def get_counter(self, fn2, generation, code):
- return self.counters.get((fn2, generation, code), 0)
+ def get_counter(self, fn2, generation, label):
+ return self.counters.get((fn2, generation, label), 0)
- def get_total(self, code):
- return self.counters.get(code, 0)
+ def get_total(self, label):
+ return self.totals.get(label, 0)
@property
def failed(self):
@@ -220,42 +216,89 @@ class Host(object):
if self.graph is None:
self.graph = copy.copy(elt)
-class Session(dict):
+class Session(object):
+
+ def __init__(self):
+ self.hosts = {}
+
+ if opt["input_file"] is None:
+ self.root = ElementTree(file = sys.stdin).getroot()
+ else:
+ self.root = ElementTree(file = opt["input_file"]).getroot()
+
+ self.rcynic_version = self.root.get("rcynic-version")
+ self.rcynic_date = self.root.get("date")
+ self.timestamp = parse_utc(self.rcynic_date)
+
+ self.labels = [Label(elt) for elt in self.root.find("labels")]
+ label_map = dict((label.code, label) for label in self.labels)
+
+ self.validation_status = [Validation_Status(elt, label_map)
+ for elt in self.root.findall("validation_status")]
+
+ if opt["suppress-backup-whining"]:
+ accepted_current = set(v.uri for v in self.validation_status
+ if v.is_current and v.accepted)
+ self.validation_status = [v for v in self.validation_status
+ if not v.is_backup
+ or v.uri not in accepted_current]
+
+ for elt in self.root.findall("rsync_history"):
+ self.add_connection(elt)
- def __init__(self, timestamp):
- dict.__init__(self)
- self.timestamp = timestamp
+ fn2s = set()
+ generations = set()
+
+ for v in self.validation_status:
+ self.maybe_add_host(v.hostname).add_validation_status(v)
+ fn2s.add(v.fn2)
+ generations.add(v.generation)
+
+ if opt["suppress-zero-columns"]:
+ self.labels = [l for l in self.labels if l.count > 0]
+
+ self.unique_hostnames = sorted(self.hosts)
+ self.unique_fn2s = sorted(fn2s)
+ self.unique_generations = sorted(generations)
def maybe_add_host(self, hostname):
- if hostname not in self:
- self[hostname] = Host()
- return self[hostname]
+ if hostname not in self.hosts:
+ self.hosts[hostname] = Host()
+ return self.hosts[hostname]
def add_connection(self, elt):
hostname = urlparse.urlparse(elt.text.strip()).hostname
self.maybe_add_host(hostname).add_connection(elt)
- def add_validation_status(self, v):
- self.maybe_add_host(v.hostname).add_validation_status(v)
+ def get_sum(self, fn2, generation, label):
+ return sum(h.get_counter(fn2, generation, label)
+ for h in self.hosts.itervalues())
+
+ def graph(self):
+ self.rrd_update()
+ self.rrd_graph()
- def run(self, *cmd):
+ def rrd_run(self, cmd):
try:
- return subprocess.check_output([str(i) for i in (opt["rrdtool-binary"],) + cmd]).splitlines()
+ cmd = [str(i) for i in cmd]
+ cmd.insert(0, opt["rrdtool-binary"])
+ return subprocess.check_output(cmd).splitlines()
except OSError, e:
- usage("Problem running %s, perhaps you need to set --rrdtool-binary? (%s)" % (opt["rrdtool-binary"], e))
+ usage("Problem running %s, perhaps you need to set --rrdtool-binary? (%s)" % (
+ opt["rrdtool-binary"], e))
rras = tuple("RRA:AVERAGE:0.5:%s:9600" % steps for steps in (1, 4, 24))
- def save(self):
- for hostname, h in self.iteritems():
- filename = os.path.join(output_directory, hostname) + ".rrd"
+ def rrd_update(self):
+ for hostname, h in self.hosts.iteritems():
+ filename = os.path.join(opt["output_directory"], hostname) + ".rrd"
if not os.path.exists(filename):
cmd = ["create", filename, "--start", self.timestamp - 1, "--step", "3600"]
cmd.extend(Host.field_ds_specifiers())
cmd.extend(self.rras)
- self.run(*cmd)
- self.run("update", filename,
- "%s:%s" % (self.timestamp, ":".join(str(v) for v in h.field_values)))
+ self.rrd_run(cmd)
+ self.rrd_run(["update", filename,
+ "%s:%s" % (self.timestamp, ":".join(str(v) for v in h.field_values))])
graph_opts = (
"--width", "1200",
@@ -306,10 +349,10 @@ class Session(dict):
("month", "-31d"),
("year", "-1y"))
- def graph(self):
- for hostname in self:
- start_html("Charts for %s" % hostname)
- filebase = os.path.join(output_directory, hostname)
+ def rrd_graph(self):
+ for hostname in self.hosts:
+ html = HTML("Charts for %s" % hostname, "%s_graphs" % hostname)
+ filebase = os.path.join(opt["output_directory"], hostname)
for period, start in self.graph_periods:
cmds = [ "graph", "%s_%s.png" % (filebase, period),
"--title", hostname,
@@ -318,230 +361,180 @@ class Session(dict):
cmds.extend(self.graph_opts)
cmds.extend(Host.field_defs(filebase))
cmds.extend(self.graph_cmds)
- imginfo = [i for i in self.run(*cmds) if i.startswith("@imginfo")]
+ imginfo = [i for i in self.rrd_run(cmds) if i.startswith("@imginfo")]
assert len(imginfo) == 1
filename, width, height = imginfo[0].split()[1:]
- SubElement(body, "h2").text = "%s over last %s" % (hostname, period)
- img = SubElement(body, "img", src = os.path.basename(filename), width = width, height = height)
- self[hostname].save_graph_maybe(img)
- SubElement(body, "br")
- SubElement(body, "a", href = "index.html").text = "Back"
- finish_html("%s_graphs" % hostname)
+ html.BodyElement("h2").text = "%s over last %s" % (hostname, period)
+ img = html.BodyElement("img", src = os.path.basename(filename), width = width, height = height)
+ self.hosts[hostname].save_graph_maybe(img)
+ html.BodyElement("br")
+ html.BodyElement("a", href = "index.html").text = "Back"
+ html.close()
#
-table_css = { "rules" : "all", "border" : "1"}
-uri_css = { "class" : "uri" }
+class HTML(object):
-def start_html(title):
+ def __init__(self, title, filebase = "index"):
- global html
- global body
+ assert filebase == "index" or opt["output_directory"] is not None
+ assert opt["output_file"] is None or opt["output_directory"] is None
- html = Element("html")
- html.append(Comment(" Generators:\n" +
- " " + input.getroot().get("rcynic-version") + "\n" +
- " $Id$\n"))
- head = SubElement(html, "head")
- body = SubElement(html, "body")
+ self.filebase = filebase
- title += " " + input.getroot().get("date")
- SubElement(head, "title").text = title
- SubElement(body, "h1").text = title
+ self.html = Element("html")
+ self.html.append(Comment(" Generators:\n" +
+ " " + session.rcynic_version + "\n" +
+ " $Id$\n"))
+ self.head = SubElement(self.html, "head")
+ self.body = SubElement(self.html, "body")
- if opt["refresh"]:
- SubElement(head, "meta", { "http-equiv" : "Refresh", "content" : str(opt["refresh"]) })
+ title += " " + session.rcynic_date
+ SubElement(self.head, "title").text = title
+ SubElement(self.body, "h1").text = title
- SubElement(head, "style", type = "text/css").text = '''
- th, td { text-align: center; padding: 4px }
- td.uri { text-align: left }
- thead tr th,
- tfoot tr td { font-weight: bold }
-'''
+ if opt["refresh"]:
+ SubElement(self.head, "meta", { "http-equiv" : "Refresh", "content" : str(opt["refresh"]) })
- if opt["use-colors"]:
- SubElement(head, "style", type = "text/css").text = '''
- .good { background-color: #77ff77 }
- .warn { background-color: yellow }
- .bad { background-color: #ff5500 }
+ SubElement(self.head, "style", type = "text/css").text = '''
+ table { rules : all; border: 1 }
+ th, td { text-align: center; padding: 4px }
+ td.uri { text-align: left }
+ thead tr th,
+ tfoot tr td { font-weight: bold }
'''
-def finish_html(name = "index"):
- global html
- global body
- assert name == "index" or output_directory is not None
- assert output_file is None or output_directory is None
- if output_file is not None:
- output = output_file
- elif output_directory is not None:
- output = os.path.join(output_directory, name + ".html")
- else:
- output = sys.stdout
- ElementTree(element = html).write(output)
- html = None
- body = None
-
-# Main
-
-os.putenv("TZ", "UTC")
-time.tzset()
-
-input = ElementTree(file = sys.stdin if input_file is None else input_file)
-
-session = Session(parse_utc(input.getroot().get("date")))
-
-labels = [Label(elt, i) for i, elt in enumerate(input.find("labels"))]
-
-Validation_Status.set_label_map(labels)
-validation_status = [Validation_Status(elt) for elt in input.findall("validation_status")]
-
-if opt["suppress-backup-whining"]:
- accepted_current = set(v.uri for v in validation_status if v.is_current and v.accepted)
- validation_status = [v for v in validation_status if not v.is_backup or v.uri not in accepted_current]
- del accepted_current
-
-for elt in input.findall("rsync_history"):
- session.add_connection(elt)
-
-for v in validation_status:
- v.stand_up_and_be_counted()
- session.add_validation_status(v)
-
-if opt["suppress-zero-columns"]:
- labels = [l for l in labels if l.sum > 0]
-
-if opt["show-graphs"]:
- session.save()
- session.graph()
-
-if not opt["one-file-per-section"]:
- start_html("rcynic summary")
-
-if opt["show-summary"]:
+ if opt["use-colors"]:
+ SubElement(self.head, "style", type = "text/css").text = '''
+ .good { background-color: #77ff77 }
+ .warn { background-color: yellow }
+ .bad { background-color: #ff5500 }
+'''
- unique_hostnames = sorted(set(v.hostname for v in validation_status))
- unique_fn2s = sorted(set(v.fn2 for v in validation_status))
- unique_generations = sorted(set(v.generation for v in validation_status))
+ def close(self):
+ if opt["output_file"] is not None:
+ output = opt["output_file"]
+ elif opt["output_directory"] is not None:
+ output = os.path.join(opt["output_directory"], self.filebase + ".html")
+ else:
+ output = sys.stdout
+ ElementTree(element = self.html).write(output)
- if opt["one-file-per-section"]:
- start_html("Grand Totals")
- else:
- SubElement(body, "br")
- SubElement(body, "h2").text = "Grand Totals"
-
- table = SubElement(body, "table", table_css)
- thead = SubElement(table, "thead")
- tfoot = SubElement(table, "tfoot")
- tbody = SubElement(table, "tbody")
- tr = SubElement(thead, "tr")
- SubElement(tr, "th")
- for l in labels:
- SubElement(tr, "th").text = l.text
- for fn2 in unique_fn2s:
- for generation in unique_generations:
- counters = [sum(h.get_counter(fn2, generation, l.code) for h in session.itervalues()) for l in labels]
- if sum(counters) > 0:
- tr = SubElement(tbody, "tr")
- SubElement(tr, "td").text = ((generation or "") + " " + (fn2 or "")).strip()
- for l, c in zip(labels, counters):
- td = SubElement(tr, "td")
- if c > 0:
- td.set("class", l.mood)
- td.text = str(c)
- tr = SubElement(tfoot, "tr")
- SubElement(tr, "td").text = "Total"
- for l in labels:
- SubElement(tr, "td", { "class" : l.mood }).text = str(l.sum)
-
- if opt["one-file-per-section"]:
- finish_html("grand_totals")
- else:
- SubElement(body, "br")
- SubElement(body, "h2").text = "Summaries by Repository Host"
+ def BodyElement(self, tag, **attrib):
+ return SubElement(self.body, tag, **attrib)
- for hostname in unique_hostnames:
- if opt["one-file-per-section"]:
- start_html("Summary for %s" % hostname)
- else:
- SubElement(body, "br")
- SubElement(body, "h3").text = hostname
- table = SubElement(body, "table", table_css)
+ def counter_table(self, data_func, total_func):
+ table = self.BodyElement("table", rules = "all", border = "1")
thead = SubElement(table, "thead")
tfoot = SubElement(table, "tfoot")
tbody = SubElement(table, "tbody")
tr = SubElement(thead, "tr")
SubElement(tr, "th")
- for l in labels:
- SubElement(tr, "th").text = l.text
- for fn2 in unique_fn2s:
- for generation in unique_generations:
- counters = [session[hostname].get_counter(fn2, generation, l.code) for l in labels]
+ for label in session.labels:
+ SubElement(tr, "th").text = label.text
+ for fn2 in session.unique_fn2s:
+ for generation in session.unique_generations:
+ counters = [data_func(fn2, generation, label) for label in session.labels]
if sum(counters) > 0:
tr = SubElement(tbody, "tr")
SubElement(tr, "td").text = ((generation or "") + " " + (fn2 or "")).strip()
- for l, c in zip(labels, counters):
+ for label, count in zip(session.labels, counters):
td = SubElement(tr, "td")
- if c > 0:
- td.set("class", l.mood)
- td.text = str(c)
+ if count > 0:
+ td.set("class", label.mood)
+ td.text = str(count)
tr = SubElement(tfoot, "tr")
SubElement(tr, "td").text = "Total"
- counters = [session[hostname].get_total(l.code) for l in labels]
- for l, c in zip(labels, counters):
+ counters = [total_func(label) for label in session.labels]
+ for lable, count in zip(session.labels, counters):
td = SubElement(tr, "td")
- if c > 0:
- td.set("class", l.mood)
- td.text = str(c)
- if opt["show-graphs"]:
- SubElement(body, "br")
- SubElement(body, "a", href = "%s_graphs.html" % hostname).append(session[hostname].graph)
- if opt["one-file-per-section"]:
- finish_html("%s_summary" % hostname)
-
-if opt["show-problems"]:
+ if count > 0:
+ td.set("class", label.mood)
+ td.text = str(count)
- if opt["one-file-per-section"]:
- start_html("Problems")
- else:
- SubElement(body, "br")
- SubElement(body, "h2").text = "Problems"
- table = SubElement(body, "table", table_css)
- thead = SubElement(table, "thead")
- tbody = SubElement(table, "tbody")
- tr = SubElement(thead, "tr")
- SubElement(tr, "th").text = "Status"
- SubElement(tr, "th").text = "URI"
- for v in validation_status:
- if v.mood != "good":
+ def detail_table(self, validation_status):
+ table = self.BodyElement("table", rules = "all", border = "1")
+ thead = SubElement(table, "thead")
+ tbody = SubElement(table, "tbody")
+ tr = SubElement(thead, "tr")
+ if opt["show-timestamps"]:
+ SubElement(tr, "th").text = "Timestamp"
+ SubElement(tr, "th").text = "Generation"
+ SubElement(tr, "th").text = "Status"
+ SubElement(tr, "th").text = "URI"
+ for v in validation_status:
tr = SubElement(tbody, "tr", { "class" : v.mood })
+ if opt["show-timestamps"]:
+ SubElement(tr, "td").text = v.timestamp
+ SubElement(tr, "td").text = v.generation
SubElement(tr, "td").text = v.label.text
- SubElement(tr, "td", uri_css).text = v.uri
- if opt["one-file-per-section"]:
- finish_html("problems")
+ SubElement(tr, "td", { "class" : "uri"}).text = v.uri
-if opt["show-detailed-status"]:
- if opt["one-file-per-section"]:
- start_html("Validation Status")
- else:
- SubElement(body, "br")
- SubElement(body, "h2").text = "Validation Status"
- table = SubElement(body, "table", table_css)
- thead = SubElement(table, "thead")
- tbody = SubElement(table, "tbody")
- tr = SubElement(thead, "tr")
- SubElement(tr, "th").text = "Timestamp"
- SubElement(tr, "th").text = "Generation"
- SubElement(tr, "th").text = "Status"
- SubElement(tr, "th").text = "URI"
- for v in validation_status:
- tr = SubElement(tbody, "tr", { "class" : v.mood })
- SubElement(tr, "td").text = v.timestamp
- SubElement(tr, "td").text = v.generation
- SubElement(tr, "td").text = v.label.text
- SubElement(tr, "td", uri_css).text = v.uri
- if opt["one-file-per-section"]:
- finish_html("validation_status")
-
-if not opt["one-file-per-section"]:
- finish_html()
+def main():
+
+ global session
+
+ os.putenv("TZ", "UTC")
+ time.tzset()
+
+ parse_options()
+
+ session = Session()
+
+ if opt["show-graphs"]:
+ session.graph()
+
+ if not opt["one-file-per-section"]:
+ html = HTML("rcynic summary")
+
+ if opt["show-summary"]:
+ if opt["one-file-per-section"]:
+ html = HTML("Grand Totals", "grand_totals")
+ else:
+ html.BodyElement("br")
+ html.BodyElement("h2").text = "Grand Totals"
+ html.counter_table(session.get_sum, Label.get_count)
+ if opt["one-file-per-section"]:
+ html.close()
+ else:
+ html.BodyElement("br")
+ html.BodyElement("h2").text = "Summaries by Repository Host"
+ for hostname in session.unique_hostnames:
+ if opt["one-file-per-section"]:
+ html = HTML("Summary for %s" % hostname, "%s_summary" % hostname)
+ else:
+ html.BodyElement("br")
+ html.BodyElement("h3").text = hostname
+ html.counter_table(session.hosts[hostname].get_counter, session.hosts[hostname].get_total)
+ if opt["show-graphs"]:
+ html.BodyElement("br")
+ html.BodyElement("a", href = "%s_graphs.html" % hostname).append(session.hosts[hostname].graph)
+ if opt["one-file-per-section"]:
+ html.close()
+
+ if opt["show-problems"]:
+ if opt["one-file-per-section"]:
+ html = HTML("Problems", "problems")
+ else:
+ html.BodyElement("br")
+ html.BodyElement("h2").text = "Problems"
+ html.detail_table((v for v in session.validation_status if v.mood != "good"))
+ if opt["one-file-per-section"]:
+ html.close()
+
+ if opt["show-detailed-status"]:
+ if opt["one-file-per-section"]:
+ html = HTML("Validation Status", "session.validation_status")
+ else:
+ html.BodyElement("br")
+ html.BodyElement("h2").text = "Validation Status"
+ html.detail_table(session.validation_status)
+ if opt["one-file-per-section"]:
+ html.close()
+
+ if not opt["one-file-per-section"]:
+ html.close()
+
+if __name__ == "__main__":
+ main()