summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChris St. Pierre <chris.a.st.pierre@gmail.com>2013-10-30 14:20:56 -0400
committerChris St. Pierre <chris.a.st.pierre@gmail.com>2013-10-30 14:20:56 -0400
commitbaf4866834afc4d9b3a707df0bd3811bda8e3719 (patch)
tree697a694ea33f0be38d553ad54a04f8a70f8a70bd
parentefb340a2d135e47ce8dac6c501a0e6f621b70ac8 (diff)
downloadbcfg2-baf4866834afc4d9b3a707df0bd3811bda8e3719.tar.gz
bcfg2-baf4866834afc4d9b3a707df0bd3811bda8e3719.tar.bz2
bcfg2-baf4866834afc4d9b3a707df0bd3811bda8e3719.zip
bcfg2-reports: rewrote with new option parser
-rwxr-xr-xsrc/lib/Bcfg2/Reporting/Reports.py276
-rw-r--r--src/lib/Bcfg2/Reporting/models.py2
-rwxr-xr-xsrc/sbin/bcfg2-reports280
3 files changed, 280 insertions, 278 deletions
diff --git a/src/lib/Bcfg2/Reporting/Reports.py b/src/lib/Bcfg2/Reporting/Reports.py
new file mode 100755
index 000000000..35c09a7e1
--- /dev/null
+++ b/src/lib/Bcfg2/Reporting/Reports.py
@@ -0,0 +1,276 @@
+#!/usr/bin/env python
+"""Query reporting system for client status."""
+
+import sys
+import argparse
+import datetime
+import Bcfg2.DBSettings
+
+
+def hosts_by_entry_type(clients, etype, entryspec):
+ result = []
+ for entry in entryspec:
+ for client in clients:
+ items = getattr(client.current_interaction, etype)()
+ for item in items:
+ if (item.entry_type == entry[0] and
+ item.name == entry[1]):
+ result.append(client)
+ return result
+
+
+def print_fields(fields, client, fmt, extra=None):
+ """ Prints the fields specified in fields of client, max_name
+ specifies the column width of the name column. """
+ fdata = []
+ if extra is None:
+ extra = dict()
+ for field in fields:
+ if field == 'time':
+ fdata.append(str(client.current_interaction.timestamp))
+ elif field == 'state':
+ if client.current_interaction.isclean():
+ fdata.append("clean")
+ else:
+ fdata.append("dirty")
+ elif field == 'total':
+ fdata.append(client.current_interaction.total_count)
+ elif field == 'good':
+ fdata.append(client.current_interaction.good_count)
+ elif field == 'modified':
+ fdata.append(client.current_interaction.modified_count)
+ elif field == 'extra':
+ fdata.append(client.current_interaction.extra_count)
+ elif field == 'bad':
+ fdata.append((client.current_interaction.bad_count))
+ else:
+ try:
+ fdata.append(getattr(client, field))
+ except AttributeError:
+ fdata.append(extra.get(field, "N/A"))
+
+ print(fmt % tuple(fdata))
+
+
+def print_entries(interaction, etype):
+ items = getattr(interaction, etype)()
+ for item in items:
+ print("%-70s %s" % (item.entry_type + ":" + item.name, etype))
+
+
+class _SingleHostCmd(Bcfg2.Options.Subcommand): # pylint: disable=W0223
+ """ Base class for bcfg2-reports modes that take a single host as
+ a positional argument """
+ options = [Bcfg2.Options.PositionalArgument("host")]
+
+ def get_client(self, setup):
+ from Bcfg2.Reporting.models import Client
+ try:
+ return Client.objects.select_related().get(name=setup.host)
+ except Client.DoesNotExist:
+ print("No such host: %s" % setup.host)
+ raise SystemExit(2)
+
+
+class Show(_SingleHostCmd):
+ """ Show bad, extra, modified, or all entries from a given host """
+
+ options = _SingleHostCmd.options + [
+ Bcfg2.Options.BooleanOption(
+ "-b", "--bad", help="Show bad entries from HOST"),
+ Bcfg2.Options.BooleanOption(
+ "-e", "--extra", help="Show extra entries from HOST"),
+ Bcfg2.Options.BooleanOption(
+ "-m", "--modified", help="Show modified entries from HOST")]
+
+ def run(self, setup):
+ client = self.get_client(setup)
+ show_all = not setup.bad and not setup.extra and not setup.modified
+ if setup.bad or show_all:
+ print_entries(client.current_interaction, "bad")
+ if setup.modified or show_all:
+ print_entries(client.current_interaction, "modified")
+ if setup.extra or show_all:
+ print_entries(client.current_interaction, "extra")
+
+
+class Total(_SingleHostCmd):
+ """ Show total number of managed and good entries from HOST """
+
+ def run(self, setup):
+ client = self.get_client(setup)
+ managed = client.current_interaction.total_count
+ good = client.current_interaction.good_count
+ print("Total managed entries: %d (good: %d)" % (managed, good))
+
+
+class Expire(_SingleHostCmd):
+ """ Toggle the expired/unexpired state of HOST """
+
+ def run(self, setup):
+ client = self.get_client(setup)
+ if client.expiration is None:
+ client.expiration = datetime.datetime.now()
+ print("%s expired." % client.name)
+ else:
+ client.expiration = None
+ print("%s un-expired." % client.name)
+ client.save()
+
+
+class _ClientSelectCmd(Bcfg2.Options.Subcommand):
+ """ Base class for subcommands that display lists of clients """
+ options = [
+ Bcfg2.Options.Option("--fields", metavar="FIELD,FIELD,...",
+ help="Only display the listed fields",
+ type=Bcfg2.Options.Types.comma_list,
+ default=['name', 'time', 'state'])]
+
+ def get_clients(self):
+ from Bcfg2.Reporting.models import Client
+ return Client.objects.exclude(current_interaction__isnull=True)
+
+ def display(self, result, fields, extra=None):
+ if 'name' not in fields:
+ fields.insert(0, "name")
+ if not result:
+ print("No match found")
+ return
+ if extra is None:
+ extra = dict()
+ max_name = max(len(c.name) for c in result)
+ ffmt = []
+ for field in fields:
+ if field == "name":
+ ffmt.append("%%-%ds" % max_name)
+ elif field == "time":
+ ffmt.append("%-19s")
+ else:
+ ffmt.append("%%-%ds" % len(field))
+ fmt = " ".join(ffmt)
+ print(fmt % tuple(f.title() for f in fields))
+ for client in result:
+ if not client.expiration:
+ print_fields(fields, client, fmt,
+ extra=extra.get(client, None))
+
+
+class Clients(_ClientSelectCmd):
+ """ Query hosts """
+ options = _ClientSelectCmd.options + [
+ Bcfg2.Options.BooleanOption(
+ "-c", "--clean", help="Show only clean hosts"),
+ Bcfg2.Options.BooleanOption(
+ "-d", "--dirty", help="Show only dirty hosts"),
+ Bcfg2.Options.BooleanOption(
+ "--stale",
+ help="Show hosts that haven't run in the last 24 hours")]
+
+ def run(self, setup):
+ result = []
+ show_all = not setup.stale and not setup.clean and not setup.dirty
+ for client in self.get_clients():
+ interaction = client.current_interaction
+ if (show_all or
+ (setup.stale and interaction.isstale()) or
+ (setup.clean and interaction.isclean()) or
+ (setup.dirty and not interaction.isclean())):
+ result.append(client)
+
+ self.display(result, setup.fields)
+
+
+class Entries(_ClientSelectCmd):
+ """ Query hosts by entries """
+ options = _ClientSelectCmd.options + [
+ Bcfg2.Options.BooleanOption(
+ "--badentry",
+ help="Show hosts that have bad entries that match"),
+ Bcfg2.Options.BooleanOption(
+ "--modifiedentry",
+ help="Show hosts that have modified entries that match"),
+ Bcfg2.Options.BooleanOption(
+ "--extraentry",
+ help="Show hosts that have extra entries that match"),
+ Bcfg2.Options.PathOption(
+ "--file", type=argparse.FileType('r'),
+ help="Read TYPE:NAME pairs from the specified file instead of "
+ "from the command line"),
+ Bcfg2.Options.PositionalArgument(
+ "entries", metavar="TYPE:NAME", nargs="*")]
+
+ def run(self, setup):
+ result = []
+ if setup.file:
+ try:
+ entries = [l.strip().split(":") for l in setup.file]
+ except IOError:
+ err = sys.exc_info()[1]
+ print("Cannot read entries from %s: %s" % (setup.file.name,
+ err))
+ return 2
+ else:
+ entries = [a.split(":") for a in setup.entries]
+
+ clients = self.get_clients()
+ if setup.badentry:
+ result = hosts_by_entry_type(clients, "bad", entries)
+ elif setup.modifiedentry:
+ result = hosts_by_entry_type(clients, "modified", entries)
+ elif setup.extraentry:
+ result = hosts_by_entry_type(clients, "extra", entries)
+
+ self.display(result, setup.fields)
+
+
+class Entry(_ClientSelectCmd):
+ """ Show the status of a single entry on all hosts """
+
+ options = _ClientSelectCmd.options + [
+ Bcfg2.Options.PositionalArgument(
+ "entry", metavar="TYPE:NAME", nargs=1)]
+
+ def run(self, setup):
+ from Bcfg2.Reporting.models import BaseEntry
+ result = []
+ fields = setup.fields
+ if 'state' in fields:
+ fields.remove('state')
+ fields.append("entry state")
+
+ etype, ename = setup.entry[0].split(":")
+ try:
+ entry_cls = BaseEntry.entry_from_type(etype)
+ except ValueError:
+ print("Unhandled/unknown type %s" % etype)
+ return 2
+
+ # TODO: batch fetch this. sqlite could break
+ extra = dict()
+ for client in self.get_clients():
+ ents = entry_cls.objects.filter(
+ name=ename,
+ interaction=client.current_interaction)
+ if len(ents) == 0:
+ continue
+ extra[client] = {"entry state": ents[0].get_state_display(),
+ "reason": ents[0]}
+ result.append(client)
+
+ self.display(result, fields, extra=extra)
+
+
+class CLI(Bcfg2.Options.CommandRegistry):
+ """ CLI class for bcfg2-reports """
+
+ def __init__(self):
+ Bcfg2.Options.CommandRegistry.__init__(self)
+ Bcfg2.Options.register_commands(self.__class__, globals().values())
+ parser = Bcfg2.Options.get_parser(
+ description="Query the Bcfg2 reporting subsystem",
+ components=[self])
+ parser.parse()
+
+ def run(self):
+ """ Run bcfg2-reports """
+ return self.runcommand()
diff --git a/src/lib/Bcfg2/Reporting/models.py b/src/lib/Bcfg2/Reporting/models.py
index fc9523067..0598e4d33 100644
--- a/src/lib/Bcfg2/Reporting/models.py
+++ b/src/lib/Bcfg2/Reporting/models.py
@@ -381,7 +381,7 @@ class BaseEntry(models.Model):
@classmethod
def entry_from_type(cls, etype):
- for entry_cls in ENTRY_CLASSES:
+ for entry_cls in ENTRY_TYPES:
if etype == entry_cls.ENTRY_TYPE:
return entry_cls
else:
diff --git a/src/sbin/bcfg2-reports b/src/sbin/bcfg2-reports
index f38d99435..5a3f3784d 100755
--- a/src/sbin/bcfg2-reports
+++ b/src/sbin/bcfg2-reports
@@ -1,282 +1,8 @@
#!/usr/bin/env python
"""Query reporting system for client status."""
-import os
import sys
-import datetime
-import Bcfg2.DBSettings
-from Bcfg2.Compat import ConfigParser
-from optparse import OptionParser, OptionGroup, make_option
-from Bcfg2.Reporting.models import (Client, BaseEntry)
+from Bcfg2.Reporting.Reports import CLI
-def hosts_by_entry_type(clients, etype, entryspec):
- result = []
- for entry in entryspec:
- for client in clients:
- items = getattr(client.current_interaction, etype)()
- for item in items:
- if (item.entry_type == entry[0] and
- item.name == entry[1]):
- result.append(client)
- return result
-
-def print_fields(fields, client, fmt, extra=None):
- """
- Prints the fields specified in fields of client, max_name
- specifies the column width of the name column.
- """
- fdata = []
- if extra is None:
- extra = dict()
- for field in fields:
- if field == 'time':
- fdata.append(str(client.current_interaction.timestamp))
- elif field == 'state':
- if client.current_interaction.isclean():
- fdata.append("clean")
- else:
- fdata.append("dirty")
- elif field == 'total':
- fdata.append(client.current_interaction.totalcount)
- elif field == 'good':
- fdata.append(client.current_interaction.goodcount)
- elif field == 'modified':
- fdata.append(client.current_interaction.modified_entry_count())
- elif field == 'extra':
- fdata.append(client.current_interaction.extra_entry_count())
- elif field == 'bad':
- fdata.append((client.current_interaction.badcount()))
- else:
- try:
- fdata.append(getattr(client, field))
- except:
- fdata.append(extra.get(field, "N/A"))
-
- print(fmt % tuple(fdata))
-
-def print_entries(interaction, etype):
- items = getattr(interaction, etype)()
- for item in items:
- print("%-70s %s" % (item.entry_type + ":" + item.name, etype))
-
-def main():
- parser = OptionParser(usage="%prog [options] <mode> [arg]")
-
- # single host modes
- multimodes = []
- singlemodes = []
- multimodes.append(make_option("-b", "--bad", action="store_true",
- default=False,
- help="Show bad entries from HOST"))
- multimodes.append(make_option("-e", "--extra", action="store_true",
- default=False,
- help="Show extra entries from HOST"))
- multimodes.append(make_option("-m", "--modified", action="store_true",
- default=False,
- help="Show modified entries from HOST"))
- multimodes.append(make_option("-s", "--show", action="store_true",
- default=False,
- help="Equivalent to --bad --extra --modified"))
- singlemodes.append(make_option("-t", "--total", action="store_true",
- default=False,
- help="Show total number of managed and good "
- "entries from HOST"))
- singlemodes.append(make_option("-x", "--expire", action="store_true",
- default=False,
- help="Toggle expired/unexpired state of "
- "HOST"))
- hostmodes = \
- OptionGroup(parser, "Single-Host Modes",
- "The following mode flags require a single HOST argument")
- hostmodes.add_options(multimodes)
- hostmodes.add_options(singlemodes)
- parser.add_option_group(hostmodes)
-
- # all host modes
- allhostmodes = OptionGroup(parser, "Host Selection Modes",
- "The following mode flags require no arguments")
- allhostmodes.add_option("-a", "--all", action="store_true", default=False,
- help="Show all hosts, including expired hosts")
- allhostmodes.add_option("-c", "--clean", action="store_true", default=False,
- help="Show only clean hosts")
- allhostmodes.add_option("-d", "--dirty", action="store_true", default=False,
- help="Show only dirty hosts")
- allhostmodes.add_option("--stale", action="store_true", default=False,
- help="Show hosts that haven't run in the last 24 "
- "hours")
- parser.add_option_group(allhostmodes)
-
- # entry modes
- entrymodes = \
- OptionGroup(parser, "Entry Modes",
- "The following mode flags require either any number of "
- "TYPE:NAME arguments describing entries, or the --file "
- "option")
- entrymodes.add_option("--badentry", action="store_true", default=False,
- help="Show hosts that have bad entries that match "
- "the argument")
- entrymodes.add_option("--modifiedentry", action="store_true", default=False,
- help="Show hosts that have modified entries that "
- "match the argument")
- entrymodes.add_option("--extraentry", action="store_true", default=False,
- help="Show hosts that have extra entries that match "
- "the argument")
- entrymodes.add_option("--entrystatus", action="store_true", default=False,
- help="Show the status of the named entry on all "
- "hosts. Only supports a single entry.")
- parser.add_option_group(entrymodes)
-
- # entry options
- entryopts = OptionGroup(parser, "Entry Options",
- "Options that can be used with entry modes")
- entryopts.add_option("--fields", metavar="FIELD,FIELD,...",
- help="Only display the listed fields",
- default='name,time,state')
- entryopts.add_option("--file", metavar="FILE",
- help="Read TYPE:NAME pairs from the specified file "
- "instead of the command line")
- parser.add_option_group(entryopts)
-
- options, args = parser.parse_args()
-
- # make sure we've specified exactly one mode
- mode_family = None
- mode = None
- for opt in allhostmodes.option_list + entrymodes.option_list + \
- singlemodes:
- if getattr(options, opt.dest):
- if mode is not None:
- parser.error("Only one mode can be specified; found %s and %s" %
- (mode.get_opt_string(), opt.get_opt_string()))
- mode = opt
- mode_family = parser.get_option_group(opt.get_opt_string())
-
- # you can specify more than one of --bad, --extra, --modified, --show, so
- # consider single-host options separately
- if not mode_family:
- for opt in multimodes:
- if getattr(options, opt.dest):
- mode_family = parser.get_option_group(opt.get_opt_string())
- break
-
- if not mode_family:
- parser.error("You must specify a mode")
-
- if mode_family == hostmodes:
- try:
- cname = args.pop()
- client = Client.objects.select_related().get(name=cname)
- except IndexError:
- parser.error("%s require a single HOST argument" % hostmodes.title)
- except Client.DoesNotExist:
- print("No such host: %s" % cname)
- return 2
-
- if options.expire:
- if client.expiration == None:
- client.expiration = datetime.datetime.now()
- print("Host expired.")
- else:
- client.expiration = None
- print("Host un-expired.")
- client.save()
- elif options.total:
- managed = client.current_interaction.total_count
- good = client.current_interaction.good_count
- print("Total managed entries: %d (good: %d)" % (managed, good))
- elif mode_family == hostmodes:
- if options.bad or options.show:
- print_entries(client.current_interaction, "bad")
-
- if options.modified or options.show:
- print_entries(client.current_interaction, "modified")
-
- if options.extra or options.show:
- print_entries(client.current_interaction, "extra")
- else:
- clients = Client.objects.exclude(current_interaction__isnull=True)
- result = list()
- edata = dict()
- fields = options.fields.split(',')
-
- if mode_family == allhostmodes:
- if args:
- print("%s do not take any arguments, ignoring" %
- allhostmodes.title)
-
- for client in clients:
- interaction = client.current_interaction
- if (options.all or
- (options.stale and interaction.isstale()) or
- (options.clean and interaction.isclean()) or
- (options.dirty and not interaction.isclean())):
- result.append(client)
- else:
- # entry query modes
- if options.file:
- try:
- entries = [l.strip().split(":")
- for l in open(options.file)]
- except IOError:
- err = sys.exc_info()[1]
- print("Cannot read entries from %s: %s" % (options.file,
- err))
- return 2
- elif args:
- entries = [a.split(":") for a in args]
- else:
- parser.error("%s require either a list of entries on the "
- "command line or the --file options" %
- mode_family.title)
-
- if options.badentry:
- result = hosts_by_entry_type(clients, "bad", entries)
- elif options.modifiedentry:
- result = hosts_by_entry_type(clients, "modified", entries)
- elif options.extraentry:
- result = hosts_by_entry_type(clients, "extra", entries)
- elif options.entrystatus:
- if 'state' in fields:
- fields.remove('state')
- fields.append("entry state")
-
- try:
- entry_cls = BaseEntry.entry_from_type(entries[0][0])
- except ValueError:
- print("Unhandled/unkown type %s" % entries[0][0])
- return 2
-
- # todo batch fetch this. sqlite could break
- for client in clients:
- ents = entry_cls.objects.filter(name=entries[0][1],
- interaction=client.current_interaction)
- if len(ents) == 0:
- continue
- edata[client] = {"entry state": ents[0].get_state_display(),
- "reason": ents[0]}
- result.append(client)
-
-
- if 'name' not in fields:
- fields.insert(0, "name")
- if not result:
- print("No match found")
- return
- max_name = max(len(c.name) for c in result)
- ffmt = []
- for field in fields:
- if field == "name":
- ffmt.append("%%-%ds" % max_name)
- elif field == "time":
- ffmt.append("%-19s")
- else:
- ffmt.append("%%-%ds" % len(field))
- fmt = " ".join(ffmt)
- print(fmt % tuple(f.title() for f in fields))
- for client in result:
- if not client.expiration:
- print_fields(fields, client, fmt,
- extra=edata.get(client, None))
-
-if __name__ == "__main__":
- sys.exit(main())
+if __name__ == '__main__':
+ sys.exit(CLI().run())