From baf4866834afc4d9b3a707df0bd3811bda8e3719 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Wed, 30 Oct 2013 14:20:56 -0400 Subject: bcfg2-reports: rewrote with new option parser --- src/lib/Bcfg2/Reporting/Reports.py | 276 ++++++++++++++++++++++++++++++++++++ src/lib/Bcfg2/Reporting/models.py | 2 +- src/sbin/bcfg2-reports | 280 +------------------------------------ 3 files changed, 280 insertions(+), 278 deletions(-) create mode 100755 src/lib/Bcfg2/Reporting/Reports.py (limited to 'src') 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] [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()) -- cgit v1.2.3-1-g7c22