From 23ae3d201af82292ad4e939569a50f2e32c689a3 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Thu, 5 May 2011 08:16:51 -0400 Subject: made bcfg2-lint error handling configurable on a much more granular level --- src/lib/Server/Lint/Bundles.py | 13 ++-- src/lib/Server/Lint/Comments.py | 14 +++-- src/lib/Server/Lint/Duplicates.py | 9 ++- src/lib/Server/Lint/InfoXML.py | 16 ++--- src/lib/Server/Lint/Pkgmgr.py | 5 +- src/lib/Server/Lint/RequiredAttrs.py | 6 +- src/lib/Server/Lint/Validate.py | 30 ++++----- src/lib/Server/Lint/__init__.py | 115 +++++++++++++++++++++++++++-------- src/sbin/bcfg2-lint | 85 ++++++++++++++++---------- 9 files changed, 197 insertions(+), 96 deletions(-) (limited to 'src') diff --git a/src/lib/Server/Lint/Bundles.py b/src/lib/Server/Lint/Bundles.py index 417f76c2d..e90159f7c 100644 --- a/src/lib/Server/Lint/Bundles.py +++ b/src/lib/Server/Lint/Bundles.py @@ -33,7 +33,8 @@ class Bundles(Bcfg2.Server.Lint.ServerPlugin): genshibundle = "%s.genshi" % bundle if (xmlbundle not in allbundles and genshibundle not in allbundles): - self.LintError("Bundle %s referenced, but does not exist" % + self.LintError("bundle-not-found", + "Bundle %s referenced, but does not exist" % bundle) def bundle_names(self, bundle): @@ -47,8 +48,9 @@ class Bundles(Bcfg2.Server.Lint.ServerPlugin): fname = bundle.name.split('Bundler/')[1].split('.')[0] bname = xdata.get('name') if fname != bname: - self.LintWarning("Inconsistent bundle name: filename is %s, bundle name is %s" % - (fname, bname)) + self.LintError("inconsistent-bundle-name", + "Inconsistent bundle name: filename is %s, bundle name is %s" % + (fname, bname)) def sgenshi_groups(self, bundle): """ ensure that Genshi Bundles do not include tags, @@ -57,5 +59,6 @@ class Bundles(Bcfg2.Server.Lint.ServerPlugin): groups = [self.RenderXML(g) for g in xdata.getroottree().findall("//Group")] if groups: - self.LintWarning(" tag is not allowed in SGenshi Bundle:\n%s" % - "\n".join(groups)) + self.LintError("group-tag-not-allowed", + " tag is not allowed in SGenshi Bundle:\n%s" % + "\n".join(groups)) diff --git a/src/lib/Server/Lint/Comments.py b/src/lib/Server/Lint/Comments.py index 8c83545b3..8e86cc564 100644 --- a/src/lib/Server/Lint/Comments.py +++ b/src/lib/Server/Lint/Comments.py @@ -143,12 +143,14 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin): unexpanded = [keyword for (keyword, status) in found.items() if status is None] if unexpanded: - self.LintWarning("%s: Required keywords(s) found but not expanded: %s" % - (filename, ", ".join(unexpanded))) + self.LintError("unexpanded-keywords", + "%s: Required keywords(s) found but not expanded: %s" % + (filename, ", ".join(unexpanded))) missing = [keyword for (keyword, status) in found.items() if status is False] if missing: - self.LintError("%s: Required keywords(s) not found: $%s$" % + self.LintError("keywords-not-found", + "%s: Required keywords(s) not found: $%s$" % (filename, "$, $".join(missing))) # next, check for required comments. found is just @@ -163,7 +165,8 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin): missing = [comment for (comment, status) in found.items() if status is False] if missing: - self.LintError("%s: Required comments(s) not found: %s" % + self.LintError("comments-not-found", + "%s: Required comments(s) not found: %s" % (filename, ", ".join(missing))) def has_all_xincludes(self, mfile): @@ -177,7 +180,8 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin): xdata = lxml.etree.parse(path) for el in xdata.findall('./{http://www.w3.org/2001/XInclude}include'): if not self.has_all_xincludes(el.get('href')): - self.LintWarning("Broken XInclude chain: could not include %s" % path) + self.LintError("broken-xinclude-chain", + "Broken XInclude chain: could not include %s" % path) return False return True diff --git a/src/lib/Server/Lint/Duplicates.py b/src/lib/Server/Lint/Duplicates.py index c8b542025..517f0dd7b 100644 --- a/src/lib/Server/Lint/Duplicates.py +++ b/src/lib/Server/Lint/Duplicates.py @@ -49,7 +49,8 @@ class Duplicates(Bcfg2.Server.Lint.ServerPlugin): if el.get('name') not in seen: seen[el.get('name')] = el else: - self.LintError("Duplicate %s '%s':\n%s\n%s" % + self.LintError("duplicate-%s" % etype, + "Duplicate %s '%s':\n%s\n%s" % (etype, el.get('name'), self.RenderXML(seen[el.get('name')]), self.RenderXML(el))) @@ -59,7 +60,8 @@ class Duplicates(Bcfg2.Server.Lint.ServerPlugin): default_groups = [g for g in self.groups_xdata.findall('.//Group') if g.get('default') == 'true'] if len(default_groups) > 1: - self.LintError("Multiple default groups defined: %s" % + self.LintError("multiple-default-groups", + "Multiple default groups defined: %s" % ",".join(default_groups)) def has_all_xincludes(self, mfile): @@ -73,7 +75,8 @@ class Duplicates(Bcfg2.Server.Lint.ServerPlugin): xdata = lxml.etree.parse(path) for el in xdata.findall('./{http://www.w3.org/2001/XInclude}include'): if not self.has_all_xincludes(el.get('href')): - self.LintWarning("Broken XInclude chain: could not include %s" % path) + self.LintError("broken-xinclude-chain", + "Broken XInclude chain: could not include %s" % path) return False return True diff --git a/src/lib/Server/Lint/InfoXML.py b/src/lib/Server/Lint/InfoXML.py index 25f609902..7725ad748 100644 --- a/src/lib/Server/Lint/InfoXML.py +++ b/src/lib/Server/Lint/InfoXML.py @@ -14,9 +14,9 @@ class InfoXML(Bcfg2.Server.Lint.ServerPlugin): if (hasattr(entryset, "infoxml") and entryset.infoxml is not None): self.check_infoxml(entryset.infoxml.pnode.data) - elif ("require" in self.config and - self.config["require"].lower != "false"): - self.LintError("No info.xml found for %s" % filename) + else: + self.LintError("no-infoxml", + "No info.xml found for %s" % filename) def check_infoxml(self, xdata): for info in xdata.getroottree().findall("//Info"): @@ -26,18 +26,18 @@ class InfoXML(Bcfg2.Server.Lint.ServerPlugin): missing = [attr for attr in required if info.get(attr) is None] if missing: - self.LintError("Required attribute(s) %s not found in %s:%s" % + self.LintError("required-infoxml-attrs-missing", + "Required attribute(s) %s not found in %s:%s" % (",".join(missing), infoxml_fname, self.RenderXML(info))) - if ("require_paranoid" in self.config and - self.config["require_paranoid"].lower() == "true" and - (Bcfg2.Options.MDATA_PARANOID.value and + if ((Bcfg2.Options.MDATA_PARANOID.value and info.get("paranoid") is not None and info.get("paranoid").lower() == "false") or (not Bcfg2.Options.MDATA_PARANOID.value and (info.get("paranoid") is None or info.get("paranoid").lower() != "true"))): - self.LintError("Paranoid must be true in %s:%s" % + self.LintError("paranoid-false", + "Paranoid must be true in %s:%s" % (infoxml_fname, self.RenderXML(info))) diff --git a/src/lib/Server/Lint/Pkgmgr.py b/src/lib/Server/Lint/Pkgmgr.py index f2eb2a5f6..39c601617 100644 --- a/src/lib/Server/Lint/Pkgmgr.py +++ b/src/lib/Server/Lint/Pkgmgr.py @@ -6,7 +6,7 @@ class Pkgmgr(Bcfg2.Server.Lint.ServerPlugin): @Bcfg2.Server.Lint.returnErrors def Run(self): if 'Pkgmgr' not in self.core.plugins: - self.LintWarning("Pkgmgr server plugin is not enabled, skipping Pkgmgr lint checks") + self.logger.info("Pkgmgr server plugin is not enabled, skipping Pkgmgr lint checks") return pset = set() @@ -31,7 +31,8 @@ class Pkgmgr(Bcfg2.Server.Lint.ServerPlugin): # check if package is already listed with same # priority, type, grp if ptuple in pset: - self.LintWarning("Duplicate Package %s, priority:%s, type:%s" % + self.LintError("duplicate-package", + "Duplicate Package %s, priority:%s, type:%s" % (pkg.get('name'), priority, ptype)) else: pset.add(ptuple) diff --git a/src/lib/Server/Lint/RequiredAttrs.py b/src/lib/Server/Lint/RequiredAttrs.py index 70ce4fe0a..cbb4395c4 100644 --- a/src/lib/Server/Lint/RequiredAttrs.py +++ b/src/lib/Server/Lint/RequiredAttrs.py @@ -53,7 +53,8 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin): try: required_attrs = set(self.required_attrs[pathtype] + ['type']) except KeyError: - self.LintError("Unknown path type %s: %s" % + self.LintError("unknown-path-type", + "Unknown path type %s: %s" % (pathtype, self.RenderXML(entry))) if 'dev_type' in required_attrs: @@ -62,7 +63,8 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin): # check if major/minor are specified required_attrs |= set(['major', 'minor']) if not pathset.issuperset(required_attrs): - self.LintError("The required attributes %s are missing for %s %sin %s:\n%s" % + self.LintError("required-attrs-missing", + "The required attributes %s are missing for %s %sin %s:\n%s" % (",".join([attr for attr in required_attrs.difference(pathset)]), diff --git a/src/lib/Server/Lint/Validate.py b/src/lib/Server/Lint/Validate.py index bb5af93f4..c7a77a4fb 100644 --- a/src/lib/Server/Lint/Validate.py +++ b/src/lib/Server/Lint/Validate.py @@ -44,8 +44,9 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): schema = lxml.etree.XMLSchema(lxml.etree.parse(schemaname % schemadir)) except: - self.LintWarning("Failed to process schema %s", - schemaname % schemadir) + self.LintError("schema-failed-to-parse", + "Failed to process schema %s", + schemaname % schemadir) continue for filename in filelist: self.validate(filename, schemaname % schemadir, @@ -55,19 +56,13 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): def check_properties(self): """ check Properties files against their schemas """ - alert = self.logger.debug - if "properties_schema" in self.config: - if self.config['properties_schema'].lower().startswith('warn'): - alert = self.LintWarning - elif self.config['properties_schema'].lower().startswith('require'): - alert = self.LintError - for filename in self.filelists['props']: schemafile = "%s.xsd" % os.path.splitext(filename)[0] if os.path.exists(schemafile): self.validate(filename, schemafile) else: - alert("No schema found for %s" % filename) + self.LintError("properties-schema-not-found", + "No schema found for %s" % filename) def validate(self, filename, schemafile, schema=None): """validate a file against the given lxml.etree.Schema. @@ -77,19 +72,22 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): try: schema = lxml.etree.XMLSchema(lxml.etree.parse(schemafile)) except: - self.LintWarning("Failed to process schema %s" % schemafile) + self.LintError("schema-failed-to-parse", + "Failed to process schema %s" % schemafile) return False try: datafile = lxml.etree.parse(filename) except SyntaxError: lint = Popen(["xmllint", filename], stdout=PIPE, stderr=STDOUT) - self.LintError("%s fails to parse:\n%s" % (filename, + self.LintError("xml-failed-to-parse", + "%s fails to parse:\n%s" % (filename, lint.communicate()[0])) lint.wait() return False except IOError: - self.LintError("Failed to open file %s" % filename) + self.LintError("xml-failed-to-read", + "Failed to open file %s" % filename) return False if not schema.validate(datafile): @@ -100,7 +98,8 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): lint = Popen(cmd, stdout=PIPE, stderr=STDOUT) output = lint.communicate()[0] if lint.wait(): - self.LintError("%s fails to verify:\n%s" % (filename, output)) + self.LintError("xml-failed-to-verify", + "%s fails to verify:\n%s" % (filename, output)) return False return True @@ -141,7 +140,8 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): for fname in all_metadata: if (fname not in self.filelists['metadata:groups'] and fname not in self.filelists['metadata:clients']): - self.LintWarning("Broken XInclude chain: Could not determine file type of %s" % fname) + self.LintError("broken-xinclude-chain", + "Broken XInclude chain: Could not determine file type of %s" % fname) def get_metadata_list(self, mtype): """ get all metadata files for the specified type (clients or diff --git a/src/lib/Server/Lint/__init__.py b/src/lib/Server/Lint/__init__.py index 4e6d03fb5..3b89d1f9e 100644 --- a/src/lib/Server/Lint/__init__.py +++ b/src/lib/Server/Lint/__init__.py @@ -16,21 +16,19 @@ import Bcfg2.Logger def returnErrors(fn): """ Decorator for Run method that returns error counts """ - def run(self, *args, **kwargs): - fn(self, *args, **kwargs) - return (self.error_count, self.warning_count) - - return run + return fn class Plugin (object): """ base class for ServerlessPlugin and ServerPlugin """ - def __init__(self, config, files=None): + + def __init__(self, config, errorhandler=None, files=None): self.files = files - self.error_count = 0 - self.warning_count = 0 self.config = config - Bcfg2.Logger.setup_logging('bcfg2-info', to_syslog=False) self.logger = logging.getLogger('bcfg2-lint') + if errorhandler is None: + self.errorHandler = ErrorHandler() + else: + self.errorHandler = errorhandler def Run(self): """ run the plugin. must be overloaded by child classes """ @@ -45,21 +43,10 @@ class Plugin (object): os.path.abspath(fname) in self.files or os.path.abspath(os.path.join(self.config['repo'], fname)) in self.files) - - def LintError(self, msg): - """ log an error condition """ - self.error_count += 1 - lines = msg.splitlines() - self.logger.error("ERROR: %s" % lines.pop()) - [self.logger.error(" %s" % l) for l in lines] - - def LintWarning(self, msg): - """ log a warning condition """ - self.warning_count += 1 - lines = msg.splitlines() - self.logger.warning("WARNING: %s" % lines.pop()) - [self.logger.warning(" %s" % l) for l in lines] + def LintError(self, err, msg): + self.errorHandler.dispatch(err, msg) + def RenderXML(self, element): """render an XML element for error output -- line number prefixed, no children""" @@ -74,17 +61,95 @@ class Plugin (object): xml = lxml.etree.tostring(element).strip() return " line %s: %s" % (element.sourceline, xml) + +class ErrorHandler (object): + # how to handle different errors by default + _errors = {"no-infoxml":"warning", + "paranoid-false":"warning", + "bundle-not-found":"error", + "inconsistent-bundle-name":"warning", + "group-tag-not-allowed":"error", + "unexpanded-keywords":"warning", + "keywords-not-found":"warning", + "comments-not-found":"warning", + "broken-xinclude-chain":"warning", + "duplicate-client":"error", + "duplicate-group":"error", + "duplicate-package":"error", + "multiple-default-groups":"error", + "required-infoxml-attrs-missing":"error", + "unknown-path-type":"error", + "required-attrs-missing":"error", + "schema-failed-to-parse":"warning", + "properties-schema-not-found":"warning", + "xml-failed-to-parse":"error", + "xml-failed-to-read":"error", + "xml-failed-to-verify":"error",} + + def __init__(self, config=None): + self.errors = 0 + self.warnings = 0 + + self.logger = logging.getLogger('bcfg2-lint') + + self._handlers = {} + if config is not None: + for err, action in config.items(): + if "warn" in action: + self._handlers[err] = self.warn + elif "err" in action: + self._handlers[err] = self.error + else: + self._handlers[err] = self.debug + + for err, action in self._errors.items(): + if err not in self._handlers: + if "warn" in action: + self._handlers[err] = self.warn + elif "err" in action: + self._handlers[err] = self.error + else: + self._handlers[err] = self.debug + + def dispatch(self, err, msg): + if err in self._handlers: + self._handlers[err](msg) + self.logger.debug(" (%s)" % err) + else: + self.logger.info("Unknown error %s" % err) + + def error(self, msg): + """ log an error condition """ + self.errors += 1 + lines = msg.splitlines() + self.logger.error("ERROR: %s" % lines.pop()) + [self.logger.error(" %s" % l) for l in lines] + + def warn(self, msg): + """ log a warning condition """ + self.warnings += 1 + lines = msg.splitlines() + self.logger.warning("WARNING: %s" % lines.pop()) + [self.logger.warning(" %s" % l) for l in lines] + + def debug(self, msg): + """ log a silent/debug condition """ + lines = msg.splitlines() + [self.logger.debug("%s" % l) for l in lines] + + class ServerlessPlugin (Plugin): """ base class for plugins that are run before the server starts up (i.e., plugins that check things that may prevent the server from starting up) """ pass + class ServerPlugin (Plugin): """ base class for plugins that check things that require the running Bcfg2 server """ - def __init__(self, lintCore, config, files=None): - Plugin.__init__(self, config, files=files) + def __init__(self, lintCore, config, **kwargs): + Plugin.__init__(self, config, **kwargs) self.core = lintCore self.logger = self.core.logger self.metadata = self.core.metadata diff --git a/src/sbin/bcfg2-lint b/src/sbin/bcfg2-lint index 18632e316..6bc34433e 100755 --- a/src/sbin/bcfg2-lint +++ b/src/sbin/bcfg2-lint @@ -27,33 +27,28 @@ class Parser(ConfigParser.ConfigParser): except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): return default -def run_serverless_plugins(plugins, config=None, setup=None): +def run_serverless_plugins(plugins, config=None, setup=None, errorhandler=None): logger.debug("Running serverless plugins") - errors = (0, 0) for plugin_name, plugin in list(plugins.items()): - plugin_errors = run_plugin(plugin, plugin_name, - setup=setup, config=config, files=files) - errors = [errors[n] + plugin_errors[n] - for n in range(0, len(errors))] - return errors + run_plugin(plugin, plugin_name, errorhandler=errorhandler, + setup=setup, config=config, files=files) -def run_server_plugins(plugins, config=None, setup=None): +def run_server_plugins(plugins, config=None, setup=None, errorhandler=None): core = load_server(setup) logger.debug("Running server plugins") - errors = (0, 0) for plugin_name, plugin in list(plugins.items()): - plugin_errors = run_plugin(plugin, plugin_name, args=[core], - setup=setup, config=config, files=files) - errors = [errors[n] + plugin_errors[n] - for n in range(0, len(errors))] - return errors - -def run_plugin(plugin, plugin_name, setup=None, args=None, config=None, - files=None): + run_plugin(plugin, plugin_name, args=[core], errorhandler=errorhandler, + setup=setup, config=config, files=files) + +def run_plugin(plugin, plugin_name, setup=None, errorhandler=None, + args=None, config=None, files=None): logger.debug(" Running %s" % plugin_name) if args is None: args = [] + if errorhandler is None: + errorhandler = get_errorhandler(config) + if config is not None and config.has_section(plugin_name): args.append(dict(config.items(plugin_name), **setup)) else: @@ -62,10 +57,18 @@ def run_plugin(plugin, plugin_name, setup=None, args=None, config=None, # older versions of python do not support mixing *-magic and # non-*-magic (e.g., "plugin(*args, files=files)", so we do this # all with *-magic - kwargs = dict(files=files) + kwargs = dict(files=files, errorhandler=errorhandler) return plugin(*args, **kwargs).Run() +def get_errorhandler(config): + """ get a Bcfg2.Server.Lint.ErrorHandler object """ + if config.has_section("errors"): + conf = dict(config.items("errors")) + else: + conf = None + return Bcfg2.Server.Lint.ErrorHandler(config=conf) + def load_server(setup): """ load server """ core = Bcfg2.Server.Core.Core(setup['repo'], setup['plugins'], @@ -104,7 +107,10 @@ if __name__ == '__main__': '/etc/bcfg2-lint.conf', cmd='--lint-config', odesc='', - long_arg = True), + long_arg=True), + 'showerrors': Bcfg2.Options.Option('Show error handling', False, + cmd='--list-errors', + long_arg=True), }) setup = Bcfg2.Options.OptionParser(optinfo) setup.parse(sys.argv[1:]) @@ -117,6 +123,21 @@ if __name__ == '__main__': config = Parser() config.read(setup['config']) + if setup['showerrors']: + if config.has_section("errors"): + econf = dict(config.items("errors")) + else: + econf = dict() + + print("%-35s %-35s" % ("Error name", "Handler (Default)")) + for err, default in Bcfg2.Server.Lint.ErrorHandler._errors.items(): + if err in econf and econf[err] != default: + handler = "%s (%s)" % (econf[err], default) + else: + handler = default + print("%-35s %-35s" % (err, handler)) + raise SystemExit(0) + # get list of plugins to run if setup['args']: allplugins = setup['args'] @@ -153,19 +174,21 @@ if __name__ == '__main__': else: serverlessplugins[plugin_name] = plugin - # errors is a tuple of (errors, warnings) - errors = run_serverless_plugins(serverlessplugins, - config=config, setup=setup) + errorhandler = get_errorhandler(config) + + run_serverless_plugins(serverlessplugins, + errorhandler=errorhandler, + config=config, setup=setup) if serverplugins: - perrors = run_server_plugins(serverplugins, config=config, setup=setup) - errors = [errors[n] + perrors[n] for n in range(0, len(errors))] - - if errors[0] or errors[1] or setup['verbose']: - print("%d errors" % errors[0]) - print("%d warnings" % errors[1]) - - if errors[0]: + run_server_plugins(serverplugins, errorhandler=errorhandler, + config=config, setup=setup) + + if errorhandler.errors or errorhandler.warnings or setup['verbose']: + print("%d errors" % errorhandler.errors) + print("%d warnings" % errorhandler.warnings) + + if errorhandler.errors: raise SystemExit(2) - elif errors[1]: + elif errorhandler.warnings: raise SystemExit(3) -- cgit v1.2.3-1-g7c22