diff options
Diffstat (limited to 'src/lib/Bcfg2/Server/Lint')
-rw-r--r-- | src/lib/Bcfg2/Server/Lint/Bundler.py | 58 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Lint/Cfg.py | 117 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Lint/Comments.py | 95 | ||||
-rwxr-xr-x | src/lib/Bcfg2/Server/Lint/Genshi.py | 62 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Lint/GroupNames.py | 14 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Lint/GroupPatterns.py | 43 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Lint/InfoXML.py | 29 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Lint/MergeFiles.py | 103 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Lint/Metadata.py | 171 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Lint/Pkgmgr.py | 49 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Lint/RequiredAttrs.py | 31 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Lint/TemplateHelper.py | 80 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Lint/Validate.py | 48 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Lint/__init__.py | 205 |
14 files changed, 929 insertions, 176 deletions
diff --git a/src/lib/Bcfg2/Server/Lint/Bundler.py b/src/lib/Bcfg2/Server/Lint/Bundler.py new file mode 100644 index 000000000..0caf4d7ed --- /dev/null +++ b/src/lib/Bcfg2/Server/Lint/Bundler.py @@ -0,0 +1,58 @@ +""" ``bcfg2-lint`` plugin for :ref:`Bundler +<server-plugins-structures-bundler-index>` """ + +from Bcfg2.Server.Lint import ServerPlugin + + +class Bundler(ServerPlugin): + """ Perform various :ref:`Bundler + <server-plugins-structures-bundler-index>` checks. """ + + def Run(self): + self.missing_bundles() + for bundle in self.core.plugins['Bundler'].entries.values(): + if self.HandlesFile(bundle.name): + self.bundle_names(bundle) + + @classmethod + def Errors(cls): + return {"bundle-not-found": "error", + "unused-bundle": "warning", + "explicit-bundle-name": "error", + "genshi-extension-bundle": "error"} + + def missing_bundles(self): + """ Find bundles listed in Metadata but not implemented in + Bundler. """ + if self.files is None: + # when given a list of files on stdin, this check is + # useless, so skip it + groupdata = self.metadata.groups_xml.xdata + ref_bundles = set([b.get("name") + for b in groupdata.findall("//Bundle")]) + + allbundles = self.core.plugins['Bundler'].bundles.keys() + for bundle in ref_bundles: + if bundle not in allbundles: + self.LintError("bundle-not-found", + "Bundle %s referenced, but does not exist" % + bundle) + + for bundle in allbundles: + if bundle not in ref_bundles: + self.LintError("unused-bundle", + "Bundle %s defined, but is not referenced " + "in Metadata" % bundle) + + def bundle_names(self, bundle): + """ Verify that deprecated bundle .genshi bundles and explicit + bundle names aren't used """ + if bundle.xdata.get('name'): + self.LintError("explicit-bundle-name", + "Deprecated explicit bundle name in %s" % + bundle.name) + + if bundle.name.endswith(".genshi"): + self.LintError("genshi-extension-bundle", + "Bundle %s uses deprecated .genshi extension" % + bundle.name) diff --git a/src/lib/Bcfg2/Server/Lint/Cfg.py b/src/lib/Bcfg2/Server/Lint/Cfg.py new file mode 100644 index 000000000..7716cd5c7 --- /dev/null +++ b/src/lib/Bcfg2/Server/Lint/Cfg.py @@ -0,0 +1,117 @@ +""" ``bcfg2-lint`` plugin for :ref:`Cfg +<server-plugins-generators-cfg>` """ + +import os +import Bcfg2.Options +from fnmatch import fnmatch +from Bcfg2.Server.Lint import ServerPlugin +from Bcfg2.Server.Plugins.Cfg import CfgGenerator + + +class Cfg(ServerPlugin): + """ warn about Cfg issues """ + + def Run(self): + for basename, entry in list(self.core.plugins['Cfg'].entries.items()): + self.check_pubkey(basename, entry) + self.check_missing_files() + self.check_conflicting_handlers() + + @classmethod + def Errors(cls): + return {"no-pubkey-xml": "warning", + "unknown-cfg-files": "error", + "extra-cfg-files": "error", + "multiple-global-handlers": "error"} + + def check_conflicting_handlers(self): + """ Check that a single entryset doesn't have multiple + non-specific (i.e., 'all') handlers. """ + cfg = self.core.plugins['Cfg'] + for eset in cfg.entries.values(): + alls = [e for e in eset.entries.values() + if (e.specific.all and + issubclass(e.__class__, CfgGenerator))] + if len(alls) > 1: + self.LintError("multiple-global-handlers", + "%s has multiple global handlers: %s" % + (eset.path, ", ".join(os.path.basename(e.name) + for e in alls))) + + def check_pubkey(self, basename, entry): + """ check that privkey.xml files have corresponding pubkey.xml + files """ + if "privkey.xml" not in entry.entries: + return + privkey = entry.entries["privkey.xml"] + if not self.HandlesFile(privkey.name): + return + + pubkey = basename + ".pub" + if pubkey not in self.core.plugins['Cfg'].entries: + self.LintError("no-pubkey-xml", + "%s has no corresponding pubkey.xml at %s" % + (basename, pubkey)) + else: + pubset = self.core.plugins['Cfg'].entries[pubkey] + if "pubkey.xml" not in pubset.entries: + self.LintError("no-pubkey-xml", + "%s has no corresponding pubkey.xml at %s" % + (basename, pubkey)) + + def _list_path_components(self, path): + """ Get a list of all components of a path. E.g., + ``self._list_path_components("/foo/bar/foobaz")`` would return + ``["foo", "bar", "foo", "baz"]``. The list is not guaranteed + to be in order.""" + rv = [] + remaining, component = os.path.split(path) + while component != '': + rv.append(component) + remaining, component = os.path.split(remaining) + return rv + + def check_missing_files(self): + """ check that all files on the filesystem are known to Cfg """ + cfg = self.core.plugins['Cfg'] + + # first, collect ignore patterns from handlers + ignore = set() + for hdlr in Bcfg2.Options.setup.cfg_handlers: + ignore.update(hdlr.__ignore__) + + # next, get a list of all non-ignored files on the filesystem + all_files = set() + for root, _, files in os.walk(cfg.data): + for fname in files: + fpath = os.path.join(root, fname) + # check against the handler ignore patterns and the + # global FAM ignore list + if (not any(fname.endswith("." + i) for i in ignore) and + not any(fnmatch(fpath, p) + for p in Bcfg2.Options.setup.ignore_files) and + not any(fnmatch(c, p) + for p in Bcfg2.Options.setup.ignore_files + for c in self._list_path_components(fpath))): + all_files.add(fpath) + + # next, get a list of all files known to Cfg + cfg_files = set() + for root, eset in cfg.entries.items(): + cfg_files.update(os.path.join(cfg.data, root.lstrip("/"), fname) + for fname in eset.entries.keys()) + + # finally, compare the two + unknown_files = all_files - cfg_files + extra_files = cfg_files - all_files + if unknown_files: + self.LintError( + "unknown-cfg-files", + "Files on the filesystem could not be understood by Cfg: %s" % + "; ".join(unknown_files)) + if extra_files: + self.LintError( + "extra-cfg-files", + "Cfg has entries for files that do not exist on the " + "filesystem: %s\nThis is probably a bug." % + "; ".join(extra_files)) diff --git a/src/lib/Bcfg2/Server/Lint/Comments.py b/src/lib/Bcfg2/Server/Lint/Comments.py index f028e225e..e2d1ec597 100644 --- a/src/lib/Bcfg2/Server/Lint/Comments.py +++ b/src/lib/Bcfg2/Server/Lint/Comments.py @@ -2,6 +2,7 @@ import os import lxml.etree +import Bcfg2.Options import Bcfg2.Server.Lint from Bcfg2.Server import XI_NAMESPACE from Bcfg2.Server.Plugins.Cfg.CfgPlaintextGenerator \ @@ -16,6 +17,81 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin): give information about the files. For instance, you can require SVN keywords in a comment, or require the name of the maintainer of a Genshi template, and so on. """ + + options = Bcfg2.Server.Lint.ServerPlugin.options + [ + Bcfg2.Options.Option( + cf=("Comments", "global_keywords"), + type=Bcfg2.Options.Types.comma_list, default=[], + help="Required keywords for all file types"), + Bcfg2.Options.Option( + cf=("Comments", "global_comments"), + type=Bcfg2.Options.Types.comma_list, default=[], + help="Required comments for all file types"), + Bcfg2.Options.Option( + cf=("Comments", "bundler_keywords"), + type=Bcfg2.Options.Types.comma_list, default=[], + help="Required keywords for non-templated bundles"), + Bcfg2.Options.Option( + cf=("Comments", "bundler_comments"), + type=Bcfg2.Options.Types.comma_list, default=[], + help="Required comments for non-templated bundles"), + Bcfg2.Options.Option( + cf=("Comments", "genshibundler_keywords"), + type=Bcfg2.Options.Types.comma_list, default=[], + help="Required keywords for templated bundles"), + Bcfg2.Options.Option( + cf=("Comments", "genshibundler_comments"), + type=Bcfg2.Options.Types.comma_list, default=[], + help="Required comments for templated bundles"), + Bcfg2.Options.Option( + cf=("Comments", "properties_keywords"), + type=Bcfg2.Options.Types.comma_list, default=[], + help="Required keywords for Properties files"), + Bcfg2.Options.Option( + cf=("Comments", "properties_comments"), + type=Bcfg2.Options.Types.comma_list, default=[], + help="Required comments for Properties files"), + Bcfg2.Options.Option( + cf=("Comments", "cfg_keywords"), + type=Bcfg2.Options.Types.comma_list, default=[], + help="Required keywords for non-templated Cfg files"), + Bcfg2.Options.Option( + cf=("Comments", "cfg_comments"), + type=Bcfg2.Options.Types.comma_list, default=[], + help="Required comments for non-templated Cfg files"), + Bcfg2.Options.Option( + cf=("Comments", "genshi_keywords"), + type=Bcfg2.Options.Types.comma_list, default=[], + help="Required keywords for Genshi-templated Cfg files"), + Bcfg2.Options.Option( + cf=("Comments", "genshi_comments"), + type=Bcfg2.Options.Types.comma_list, default=[], + help="Required comments for Genshi-templated Cfg files"), + Bcfg2.Options.Option( + cf=("Comments", "cheetah_keywords"), + type=Bcfg2.Options.Types.comma_list, default=[], + help="Required keywords for Cheetah-templated Cfg files"), + Bcfg2.Options.Option( + cf=("Comments", "cheetah_comments"), + type=Bcfg2.Options.Types.comma_list, default=[], + help="Required comments for Cheetah-templated Cfg files"), + Bcfg2.Options.Option( + cf=("Comments", "infoxml_keywords"), + type=Bcfg2.Options.Types.comma_list, default=[], + help="Required keywords for info.xml files"), + Bcfg2.Options.Option( + cf=("Comments", "infoxml_comments"), + type=Bcfg2.Options.Types.comma_list, default=[], + help="Required comments for info.xml files"), + Bcfg2.Options.Option( + cf=("Comments", "probe_keywords"), + type=Bcfg2.Options.Types.comma_list, default=[], + help="Required keywords for probes"), + Bcfg2.Options.Option( + cf=("Comments", "probe_comments"), + type=Bcfg2.Options.Types.comma_list, default=[], + help="Required comments for probes")] + def __init__(self, *args, **kwargs): Bcfg2.Server.Lint.ServerPlugin.__init__(self, *args, **kwargs) self.config_cache = {} @@ -73,17 +149,14 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin): if rtype not in self.config_cache[itype]: rv = [] - global_item = "global_%ss" % itype - if global_item in self.config: - rv.extend(self.config[global_item].split(",")) - - item = "%s_%ss" % (rtype.lower(), itype) - if item in self.config: - if self.config[item]: - rv.extend(self.config[item].split(",")) - else: - # config explicitly specifies nothing - rv = [] + rv.extend(getattr(Bcfg2.Options.setup, "global_%ss" % itype)) + local_reqs = getattr(Bcfg2.Options.setup, + "%s_%ss" % (rtype.lower(), itype)) + if local_reqs == ['']: + # explicitly specified as empty + rv = [] + else: + rv.extend(local_reqs) self.config_cache[itype][rtype] = rv return self.config_cache[itype][rtype] diff --git a/src/lib/Bcfg2/Server/Lint/Genshi.py b/src/lib/Bcfg2/Server/Lint/Genshi.py index 1ecb6da42..a2581e70b 100755 --- a/src/lib/Bcfg2/Server/Lint/Genshi.py +++ b/src/lib/Bcfg2/Server/Lint/Genshi.py @@ -4,7 +4,6 @@ import sys import Bcfg2.Server.Lint from genshi.template import TemplateLoader, NewTextTemplate, MarkupTemplate, \ TemplateSyntaxError -from Bcfg2.Server.Plugins.Bundler import BundleTemplateFile from Bcfg2.Server.Plugins.Cfg.CfgGenshiGenerator import CfgGenshiGenerator @@ -14,60 +13,41 @@ class Genshi(Bcfg2.Server.Lint.ServerPlugin): def Run(self): if 'Cfg' in self.core.plugins: self.check_cfg() - if 'TGenshi' in self.core.plugins: - self.check_tgenshi() if 'Bundler' in self.core.plugins: self.check_bundler() @classmethod def Errors(cls): - return {"genshi-syntax-error": "error"} + return {"genshi-syntax-error": "error", + "unknown-genshi-error": "error"} + + def check_template(self, loader, fname, cls=None): + """ Generic check for all genshi templates (XML and text) """ + try: + loader.load(fname, cls=cls) + except TemplateSyntaxError: + err = sys.exc_info()[1] + self.LintError("genshi-syntax-error", + "Genshi syntax error in %s: %s" % (fname, err)) + except: + err = sys.exc_info()[1] + self.LintError("unknown-genshi-error", + "Unknown Genshi error in %s: %s" % (fname, err)) def check_cfg(self): """ Check genshi templates in Cfg for syntax errors. """ for entryset in self.core.plugins['Cfg'].entries.values(): for entry in entryset.entries.values(): if (self.HandlesFile(entry.name) and - isinstance(entry, CfgGenshiGenerator) and - not entry.template): - try: - entry.loader.load(entry.name, - cls=NewTextTemplate) - except TemplateSyntaxError: - err = sys.exc_info()[1] - self.LintError("genshi-syntax-error", - "Genshi syntax error: %s" % err) - except: - etype, err = sys.exc_info()[:2] - self.LintError( - "genshi-syntax-error", - "Unexpected Genshi error on %s: %s: %s" % - (entry.name, etype.__name__, err)) - - def check_tgenshi(self): - """ Check templates in TGenshi for syntax errors. """ - loader = TemplateLoader() - - for eset in self.core.plugins['TGenshi'].entries.values(): - for fname, sdata in list(eset.entries.items()): - if self.HandlesFile(fname): - try: - loader.load(sdata.name, cls=NewTextTemplate) - except TemplateSyntaxError: - err = sys.exc_info()[1] - self.LintError("genshi-syntax-error", - "Genshi syntax error: %s" % err) + isinstance(entry, CfgGenshiGenerator) and + not entry.template): + self.check_template(entry.loader, entry.name, + cls=NewTextTemplate) def check_bundler(self): """ Check templates in Bundler for syntax errors. """ loader = TemplateLoader() - for entry in self.core.plugins['Bundler'].entries.values(): if (self.HandlesFile(entry.name) and - isinstance(entry, BundleTemplateFile)): - try: - loader.load(entry.name, cls=MarkupTemplate) - except TemplateSyntaxError: - err = sys.exc_info()[1] - self.LintError("genshi-syntax-error", - "Genshi syntax error: %s" % err) + entry.template is not None): + self.check_template(loader, entry.name, cls=MarkupTemplate) diff --git a/src/lib/Bcfg2/Server/Lint/GroupNames.py b/src/lib/Bcfg2/Server/Lint/GroupNames.py index b180083d5..e28080300 100644 --- a/src/lib/Bcfg2/Server/Lint/GroupNames.py +++ b/src/lib/Bcfg2/Server/Lint/GroupNames.py @@ -3,11 +3,6 @@ import os import re import Bcfg2.Server.Lint -try: - from Bcfg2.Server.Plugins.Bundler import BundleTemplateFile - HAS_GENSHI = True -except ImportError: - HAS_GENSHI = False class GroupNames(Bcfg2.Server.Lint.ServerPlugin): @@ -44,14 +39,13 @@ class GroupNames(Bcfg2.Server.Lint.ServerPlugin): continue xdata = rules.pnode.data self.check_entries(xdata.xpath("//Group"), - os.path.join(self.config['repo'], rules.name)) + os.path.join(Bcfg2.Options.setup.repository, + rules.name)) def check_bundles(self): """ Check groups used in the Bundler plugin for validity. """ for bundle in self.core.plugins['Bundler'].entries.values(): - if (self.HandlesFile(bundle.name) and - (not HAS_GENSHI or - not isinstance(bundle, BundleTemplateFile))): + if self.HandlesFile(bundle.name) and bundle.template is None: self.check_entries(bundle.xdata.xpath("//Group"), bundle.name) @@ -59,7 +53,7 @@ class GroupNames(Bcfg2.Server.Lint.ServerPlugin): """ Check groups used or declared in the Metadata plugin for validity. """ self.check_entries(self.metadata.groups_xml.xdata.xpath("//Group"), - os.path.join(self.config['repo'], + os.path.join(Bcfg2.Options.setup.repository, self.metadata.groups_xml.name)) def check_grouppatterns(self): diff --git a/src/lib/Bcfg2/Server/Lint/GroupPatterns.py b/src/lib/Bcfg2/Server/Lint/GroupPatterns.py new file mode 100644 index 000000000..d8142cab9 --- /dev/null +++ b/src/lib/Bcfg2/Server/Lint/GroupPatterns.py @@ -0,0 +1,43 @@ +""" ``bcfg2-lint`` plugin for :ref:`GroupPatterns +<server-plugins-grouping-grouppatterns>` """ + +import sys +from Bcfg2.Server.Lint import ServerPlugin +from Bcfg2.Server.Plugins.GroupPatterns import PatternMap + + +class GroupPatterns(ServerPlugin): + """ ``bcfg2-lint`` plugin to check all given :ref:`GroupPatterns + <server-plugins-grouping-grouppatterns>` patterns for validity. + This is simply done by trying to create a + :class:`Bcfg2.Server.Plugins.GroupPatterns.PatternMap` object for + each pattern, and catching exceptions and presenting them as + ``bcfg2-lint`` errors.""" + + def Run(self): + cfg = self.core.plugins['GroupPatterns'].config + for entry in cfg.xdata.xpath('//GroupPattern'): + groups = [g.text for g in entry.findall('Group')] + self.check(entry, groups, ptype='NamePattern') + self.check(entry, groups, ptype='NameRange') + + @classmethod + def Errors(cls): + return {"pattern-fails-to-initialize": "error"} + + def check(self, entry, groups, ptype="NamePattern"): + """ Check a single pattern for validity """ + if ptype == "NamePattern": + pmap = lambda p: PatternMap(p, None, groups) + else: + pmap = lambda p: PatternMap(None, p, groups) + + for el in entry.findall(ptype): + pat = el.text + try: + pmap(pat) + except: # pylint: disable=W0702 + err = sys.exc_info()[1] + self.LintError("pattern-fails-to-initialize", + "Failed to initialize %s %s for %s: %s" % + (ptype, pat, entry.get('pattern'), err)) diff --git a/src/lib/Bcfg2/Server/Lint/InfoXML.py b/src/lib/Bcfg2/Server/Lint/InfoXML.py index 95657317e..4b1513a11 100644 --- a/src/lib/Bcfg2/Server/Lint/InfoXML.py +++ b/src/lib/Bcfg2/Server/Lint/InfoXML.py @@ -4,7 +4,6 @@ import os import Bcfg2.Options import Bcfg2.Server.Lint from Bcfg2.Server.Plugins.Cfg.CfgInfoXML import CfgInfoXML -from Bcfg2.Server.Plugins.Cfg.CfgLegacyInfo import CfgLegacyInfo class InfoXML(Bcfg2.Server.Lint.ServerPlugin): @@ -16,6 +15,15 @@ class InfoXML(Bcfg2.Server.Lint.ServerPlugin): * Paranoid mode disabled in an ``info.xml`` file; * Required attributes missing from ``info.xml`` """ + + options = Bcfg2.Server.Lint.ServerPlugin.options + [ + Bcfg2.Options.Common.default_paranoid, + Bcfg2.Options.Option( + cf=("InfoXML", "required_attrs"), + type=Bcfg2.Options.Types.comma_list, + default=["owner", "group", "mode"], + help="Attributes to require on <Info> tags")] + def Run(self): if 'Cfg' not in self.core.plugins: return @@ -27,25 +35,15 @@ class InfoXML(Bcfg2.Server.Lint.ServerPlugin): for entry in entryset.entries.values(): if isinstance(entry, CfgInfoXML): self.check_infoxml(infoxml_fname, - entry.infoxml.pnode.data) + entry.infoxml.xdata) found = True if not found: self.LintError("no-infoxml", "No info.xml found for %s" % filename) - for entry in entryset.entries.values(): - if isinstance(entry, CfgLegacyInfo): - if not self.HandlesFile(entry.path): - continue - self.LintError("deprecated-info-file", - "Deprecated %s file found at %s" % - (os.path.basename(entry.name), - entry.path)) - @classmethod def Errors(cls): return {"no-infoxml": "warning", - "deprecated-info-file": "warning", "paranoid-false": "warning", "required-infoxml-attrs-missing": "error"} @@ -53,8 +51,7 @@ class InfoXML(Bcfg2.Server.Lint.ServerPlugin): """ Verify that info.xml contains everything it should. """ for info in xdata.getroottree().findall("//Info"): required = [] - if "required_attrs" in self.config: - required = self.config["required_attrs"].split(",") + required = Bcfg2.Options.setup.required_attrs missing = [attr for attr in required if info.get(attr) is None] if missing: @@ -63,10 +60,10 @@ class InfoXML(Bcfg2.Server.Lint.ServerPlugin): (",".join(missing), fname, self.RenderXML(info))) - if ((Bcfg2.Options.MDATA_PARANOID.value and + if ((Bcfg2.Options.setup.default_paranoid == "true" and info.get("paranoid") is not None and info.get("paranoid").lower() == "false") or - (not Bcfg2.Options.MDATA_PARANOID.value and + (Bcfg2.Options.setup.default_paranoid == "false" and (info.get("paranoid") is None or info.get("paranoid").lower() != "true"))): self.LintError("paranoid-false", diff --git a/src/lib/Bcfg2/Server/Lint/MergeFiles.py b/src/lib/Bcfg2/Server/Lint/MergeFiles.py index 2419c3d43..8e6a926ae 100644 --- a/src/lib/Bcfg2/Server/Lint/MergeFiles.py +++ b/src/lib/Bcfg2/Server/Lint/MergeFiles.py @@ -8,9 +8,24 @@ import Bcfg2.Server.Lint from Bcfg2.Server.Plugins.Cfg import CfgGenerator +def threshold(val): + """ Option type processor to accept either a percentage (e.g., + "threshold=75") or a ratio (e.g., "threshold=.75") """ + rv = float(val) + if rv > 1: + rv /= 100 + return rv + + class MergeFiles(Bcfg2.Server.Lint.ServerPlugin): """ find Probes or Cfg files with multiple similar files that might be merged into one """ + + options = Bcfg2.Server.Lint.ServerPlugin.options + [ + Bcfg2.Options.Option( + cf=("MergeFiles", "threshold"), default="0.75", type=threshold, + help="The threshold at which to suggest merging files and probes")] + def Run(self): if 'Cfg' in self.core.plugins: self.check_cfg() @@ -20,14 +35,25 @@ class MergeFiles(Bcfg2.Server.Lint.ServerPlugin): @classmethod def Errors(cls): return {"merge-cfg": "warning", - "merge-probes": "warning"} + "identical-cfg": "error", + "merge-probes": "warning", + "identical-probes": "error"} def check_cfg(self): """ check Cfg for similar files """ + # ignore non-specific Cfg entries, e.g., privkey.xml + ignore = [] + for hdlr in Bcfg2.Options.setup.cfg_handlers: + if not hdlr.__specific__: + ignore.extend(hdlr.__basenames__) + for filename, entryset in self.core.plugins['Cfg'].entries.items(): candidates = dict([(f, e) for f, e in entryset.entries.items() - if isinstance(e, CfgGenerator)]) - for mset in self.get_similar(candidates): + if (isinstance(e, CfgGenerator) and + f not in ignore and + not f.endswith(".crypt"))]) + similar, identical = self.get_similar(candidates) + for mset in similar: self.LintError("merge-cfg", "The following files are similar: %s. " "Consider merging them into a single Genshi " @@ -35,54 +61,69 @@ class MergeFiles(Bcfg2.Server.Lint.ServerPlugin): ", ".join([os.path.join(filename, p) for p in mset])) + for mset in identical: + self.LintError("identical-cfg", + "The following files are identical: %s. " + "Strongly consider merging them into a single " + "Genshi template." % + ", ".join([os.path.join(filename, p) + for p in mset])) + def check_probes(self): """ check Probes for similar files """ probes = self.core.plugins['Probes'].probes.entries - for mset in self.get_similar(probes): + similar, identical = self.get_similar(probes) + for mset in similar: self.LintError("merge-probes", "The following probes are similar: %s. " "Consider merging them into a single probe." % ", ".join([p for p in mset])) + for mset in identical: + self.LintError("identical-probes", + "The following probes are identical: %s. " + "Strongly consider merging them into a single " + "probe." % + ", ".join([p for p in mset])) def get_similar(self, entries): """ Get a list of similar files from the entry dict. Return value is a list of lists, each of which gives the filenames of similar files """ - if "threshold" in self.config: - # accept threshold either as a percent (e.g., "threshold=75") or - # as a ratio (e.g., "threshold=.75") - threshold = float(self.config['threshold']) - if threshold > 1: - threshold /= 100 - else: - threshold = 0.75 - rv = [] + similar = [] + identical = [] elist = list(entries.items()) while elist: - result = self._find_similar(elist.pop(0), copy.copy(elist), - threshold) - if len(result) > 1: - elist = [(fname, fdata) - for fname, fdata in elist - if fname not in result] - rv.append(result) - return rv + rv = self._find_similar(elist.pop(0), copy.copy(elist)) + if rv[0]: + similar.append(rv[0]) + if rv[1]: + identical.append(rv[1]) + elist = [(fname, fdata) + for fname, fdata in elist + if fname not in rv[0] | rv[1]] + return similar, identical - def _find_similar(self, ftuple, others, threshold): + def _find_similar(self, ftuple, others): """ Find files similar to the one described by ftupe in the list of other files. ftuple is a tuple of (filename, data); others is a list of such tuples. threshold is a float between 0 and 1 that describes how similar two files much be to rate as 'similar' """ fname, fdata = ftuple - rv = [fname] - while others: - cname, cdata = others.pop(0) + similar = set() + identical = set() + for cname, cdata in others: seqmatch = SequenceMatcher(None, fdata.data, cdata.data) # perform progressively more expensive comparisons - if (seqmatch.real_quick_ratio() > threshold and - seqmatch.quick_ratio() > threshold and - seqmatch.ratio() > threshold): - rv.extend(self._find_similar((cname, cdata), copy.copy(others), - threshold)) - return rv + if seqmatch.real_quick_ratio() == 1.0: + identical.add(cname) + elif ( + seqmatch.real_quick_ratio() > Bcfg2.Options.setup.threshold and + seqmatch.quick_ratio() > Bcfg2.Options.setup.threshold and + seqmatch.ratio() > Bcfg2.Options.setup.threshold): + similar.add(cname) + if similar: + similar.add(fname) + if identical: + identical.add(fname) + return (similar, identical) diff --git a/src/lib/Bcfg2/Server/Lint/Metadata.py b/src/lib/Bcfg2/Server/Lint/Metadata.py new file mode 100644 index 000000000..248b1610c --- /dev/null +++ b/src/lib/Bcfg2/Server/Lint/Metadata.py @@ -0,0 +1,171 @@ +""" ``bcfg2-lint`` plugin for :ref:`Metadata +<server-plugins-grouping-metadata>` """ + +from Bcfg2.Server.Lint import ServerPlugin + + +class Metadata(ServerPlugin): + """ ``bcfg2-lint`` plugin for :ref:`Metadata + <server-plugins-grouping-metadata>`. This checks for several things: + + * ``<Client>`` tags nested inside other ``<Client>`` tags; + * Deprecated options (like ``location="floating"``); + * Profiles that don't exist, or that aren't profile groups; + * Groups or clients that are defined multiple times; + * Multiple default groups or a default group that isn't a profile + group. + """ + + def Run(self): + self.nested_clients() + self.deprecated_options() + self.bogus_profiles() + self.duplicate_groups() + self.duplicate_default_groups() + self.duplicate_clients() + self.default_is_profile() + + @classmethod + def Errors(cls): + return {"nested-client-tags": "warning", + "deprecated-clients-options": "warning", + "nonexistent-profile-group": "error", + "non-profile-set-as-profile": "error", + "duplicate-group": "error", + "duplicate-client": "error", + "multiple-default-groups": "error", + "default-is-not-profile": "error"} + + def deprecated_options(self): + """ Check for the ``location='floating'`` option, which has + been deprecated in favor of ``floating='true'``. """ + if not hasattr(self.metadata, "clients_xml"): + # using metadata database + return + clientdata = self.metadata.clients_xml.xdata + for el in clientdata.xpath("//Client"): + loc = el.get("location") + if loc: + if loc == "floating": + floating = True + else: + floating = False + self.LintError("deprecated-clients-options", + "The location='%s' option is deprecated. " + "Please use floating='%s' instead:\n%s" % + (loc, floating, self.RenderXML(el))) + + def nested_clients(self): + """ Check for a ``<Client/>`` tag inside a ``<Client/>`` tag, + which is either redundant or will never match. """ + groupdata = self.metadata.groups_xml.xdata + for el in groupdata.xpath("//Client//Client"): + self.LintError("nested-client-tags", + "Client %s nested within Client tag: %s" % + (el.get("name"), self.RenderXML(el))) + + def bogus_profiles(self): + """ Check for clients that have profiles that are either not + flagged as profile groups in ``groups.xml``, or don't exist. """ + if not hasattr(self.metadata, "clients_xml"): + # using metadata database + return + for client in self.metadata.clients_xml.xdata.findall('.//Client'): + profile = client.get("profile") + if profile not in self.metadata.groups: + self.LintError("nonexistent-profile-group", + "%s has nonexistent profile group %s:\n%s" % + (client.get("name"), profile, + self.RenderXML(client))) + elif not self.metadata.groups[profile].is_profile: + self.LintError("non-profile-set-as-profile", + "%s is set as profile for %s, but %s is not a " + "profile group:\n%s" % + (profile, client.get("name"), profile, + self.RenderXML(client))) + + def duplicate_default_groups(self): + """ Check for multiple default groups. """ + defaults = [] + for grp in self.metadata.groups_xml.xdata.xpath("//Groups/Group") + \ + self.metadata.groups_xml.xdata.xpath("//Groups/Group//Group"): + if grp.get("default", "false").lower() == "true": + defaults.append(self.RenderXML(grp)) + if len(defaults) > 1: + self.LintError("multiple-default-groups", + "Multiple default groups defined:\n%s" % + "\n".join(defaults)) + + def duplicate_clients(self): + """ Check for clients that are defined more than once. """ + if not hasattr(self.metadata, "clients_xml"): + # using metadata database + return + self.duplicate_entries( + self.metadata.clients_xml.xdata.xpath("//Client"), + "client") + + def duplicate_groups(self): + """ Check for groups that are defined more than once. There + are two ways this can happen: + + 1. The group is listed twice with contradictory options. + 2. The group is listed with no options *first*, and then with + options later. + + In this context, 'first' refers to the order in which groups + are parsed; see the loop condition below and + _handle_groups_xml_event above for details. """ + groups = dict() + duplicates = dict() + for grp in self.metadata.groups_xml.xdata.xpath("//Groups/Group") + \ + self.metadata.groups_xml.xdata.xpath("//Groups/Group//Group"): + grpname = grp.get("name") + if grpname in duplicates: + duplicates[grpname].append(grp) + elif len(grp.attrib) > 1: # group has options + if grpname in groups: + duplicates[grpname] = [grp, groups[grpname]] + else: + groups[grpname] = grp + else: # group has no options + groups[grpname] = grp + for grpname, grps in duplicates.items(): + self.LintError("duplicate-group", + "Group %s is defined multiple times:\n%s" % + (grpname, + "\n".join(self.RenderXML(g) for g in grps))) + + def duplicate_entries(self, allentries, etype): + """ Generic duplicate entry finder. + + :param allentries: A list of all entries to check for + duplicates. + :type allentries: list of lxml.etree._Element + :param etype: The entry type. This will be used to determine + the error name (``duplicate-<etype>``) and for + display to the end user. + :type etype: string + """ + entries = dict() + for el in allentries: + if el.get("name") in entries: + entries[el.get("name")].append(self.RenderXML(el)) + else: + entries[el.get("name")] = [self.RenderXML(el)] + for ename, els in entries.items(): + if len(els) > 1: + self.LintError("duplicate-%s" % etype, + "%s %s is defined multiple times:\n%s" % + (etype.title(), ename, "\n".join(els))) + + def default_is_profile(self): + """ Ensure that the default group is a profile group. """ + if (self.metadata.default and + not self.metadata.groups[self.metadata.default].is_profile): + xdata = \ + self.metadata.groups_xml.xdata.xpath("//Group[@name='%s']" % + self.metadata.default)[0] + self.LintError("default-is-not-profile", + "Default group is not a profile group:\n%s" % + self.RenderXML(xdata)) diff --git a/src/lib/Bcfg2/Server/Lint/Pkgmgr.py b/src/lib/Bcfg2/Server/Lint/Pkgmgr.py new file mode 100644 index 000000000..3f0b9477c --- /dev/null +++ b/src/lib/Bcfg2/Server/Lint/Pkgmgr.py @@ -0,0 +1,49 @@ +""" ``bcfg2-lint`` plugin for :ref:`Pkgmgr +<server-plugins-generators-pkgmgr>` """ + +import os +import glob +import lxml.etree +import Bcfg2.Options +from Bcfg2.Server.Lint import ServerlessPlugin + + +class Pkgmgr(ServerlessPlugin): + """ Find duplicate :ref:`Pkgmgr + <server-plugins-generators-pkgmgr>` entries with the same + priority. """ + + def Run(self): + pset = set() + for pfile in glob.glob(os.path.join(Bcfg2.Options.setup.repository, + 'Pkgmgr', '*.xml')): + if self.HandlesFile(pfile): + xdata = lxml.etree.parse(pfile).getroot() + # get priority, type, group + priority = xdata.get('priority') + ptype = xdata.get('type') + for pkg in xdata.xpath("//Package"): + if pkg.getparent().tag == 'Group': + grp = pkg.getparent().get('name') + if (type(grp) is not str and + grp.getparent().tag == 'Group'): + pgrp = grp.getparent().get('name') + else: + pgrp = 'none' + else: + grp = 'none' + pgrp = 'none' + ptuple = (pkg.get('name'), priority, ptype, grp, pgrp) + # check if package is already listed with same + # priority, type, grp + if ptuple in pset: + self.LintError( + "duplicate-package", + "Duplicate Package %s, priority:%s, type:%s" % + (pkg.get('name'), priority, ptype)) + else: + pset.add(ptuple) + + @classmethod + def Errors(cls): + return {"duplicate-packages": "error"} diff --git a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py index e49779a10..5d9e229fa 100644 --- a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py +++ b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py @@ -3,16 +3,10 @@ verified with an XML schema alone. """ import os import re -import lxml.etree import Bcfg2.Server.Lint import Bcfg2.Client.Tools.VCS from Bcfg2.Server.Plugins.Packages import Apt, Yum from Bcfg2.Client.Tools.POSIX.base import device_map -try: - from Bcfg2.Server.Plugins.Bundler import BundleTemplateFile - HAS_GENSHI = True -except ImportError: - HAS_GENSHI = False # format verifying functions. TODO: These should be moved into XML @@ -144,7 +138,7 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin): for source in self.core.plugins['Packages'].sources: if isinstance(source, Yum.YumSource): if (not source.pulp_id and not source.url and - not source.rawurl): + not source.rawurl): self.LintError( "required-attrs-missing", "A %s source must have either a url, rawurl, or " @@ -158,7 +152,7 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin): (source.ptype, self.RenderXML(source.xsource))) if (not isinstance(source, Apt.AptSource) and - source.recommended): + source.recommended): self.LintError( "extra-attrs", "The recommended attribute is not supported on %s sources:" @@ -173,8 +167,9 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin): for rules in self.core.plugins['Rules'].entries.values(): xdata = rules.pnode.data for path in xdata.xpath("//Path"): - self.check_entry(path, os.path.join(self.config['repo'], - rules.name)) + self.check_entry(path, + os.path.join(Bcfg2.Options.setup.repository, + rules.name)) def check_bundles(self): """ Check bundles for BoundPath entries with missing @@ -183,17 +178,9 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin): return for bundle in self.core.plugins['Bundler'].entries.values(): - if (self.HandlesFile(bundle.name) and - (not HAS_GENSHI or - not isinstance(bundle, BundleTemplateFile))): - try: - xdata = lxml.etree.XML(bundle.data) - except (lxml.etree.XMLSyntaxError, AttributeError): - xdata = \ - lxml.etree.parse(bundle.template.filepath).getroot() - - for path in \ - xdata.xpath("//*[substring(name(), 1, 5) = 'Bound']"): + if self.HandlesFile(bundle.name) and bundle.template is None: + for path in bundle.xdata.xpath( + "//*[substring(name(), 1, 5) = 'Bound']"): self.check_entry(path, bundle.name) def check_entry(self, entry, filename): @@ -241,7 +228,7 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin): fmt = required_attrs['__text__'] del required_attrs['__text__'] if (not entry.text and - not entry.get('empty', 'false').lower() == 'true'): + not entry.get('empty', 'false').lower() == 'true'): self.LintError("required-attrs-missing", "Text missing for %s %s in %s: %s" % (tag, name, filename, diff --git a/src/lib/Bcfg2/Server/Lint/TemplateHelper.py b/src/lib/Bcfg2/Server/Lint/TemplateHelper.py new file mode 100644 index 000000000..23b6da484 --- /dev/null +++ b/src/lib/Bcfg2/Server/Lint/TemplateHelper.py @@ -0,0 +1,80 @@ +""" ``bcfg2-lint`` plugin for :ref:`TemplateHelper +<server-plugins-connectors-templatehelper>` """ + +import sys +import imp +from Bcfg2.Server.Lint import ServerPlugin +from Bcfg2.Server.Plugins.TemplateHelper import HelperModule, MODULE_RE, \ + safe_module_name + + +class TemplateHelper(ServerPlugin): + """ ``bcfg2-lint`` plugin to ensure that all :ref:`TemplateHelper + <server-plugins-connectors-templatehelper>` modules are valid. + This can check for: + + * A TemplateHelper module that cannot be imported due to syntax or + other compile-time errors; + * A TemplateHelper module that does not have an ``__export__`` + attribute, or whose ``__export__`` is not a list; + * Bogus symbols listed in ``__export__``, including symbols that + don't exist, that are reserved, or that start with underscores. + """ + + def __init__(self, *args, **kwargs): + ServerPlugin.__init__(self, *args, **kwargs) + self.reserved_keywords = dir(HelperModule("foo.py")) + + def Run(self): + for helper in self.core.plugins['TemplateHelper'].entries.values(): + if self.HandlesFile(helper.name): + self.check_helper(helper.name) + + def check_helper(self, helper): + """ Check a single helper module. + + :param helper: The filename of the helper module + :type helper: string + """ + module_name = MODULE_RE.search(helper).group(1) + + try: + module = imp.load_source(safe_module_name(module_name), helper) + except: # pylint: disable=W0702 + err = sys.exc_info()[1] + self.LintError("templatehelper-import-error", + "Failed to import %s: %s" % + (helper, err)) + return + + if not hasattr(module, "__export__"): + self.LintError("templatehelper-no-export", + "%s has no __export__ list" % helper) + return + elif not isinstance(module.__export__, list): + self.LintError("templatehelper-nonlist-export", + "__export__ is not a list in %s" % helper) + return + + for sym in module.__export__: + if not hasattr(module, sym): + self.LintError("templatehelper-nonexistent-export", + "%s: exported symbol %s does not exist" % + (helper, sym)) + elif sym in self.reserved_keywords: + self.LintError("templatehelper-reserved-export", + "%s: exported symbol %s is reserved" % + (helper, sym)) + elif sym.startswith("_"): + self.LintError("templatehelper-underscore-export", + "%s: exported symbol %s starts with underscore" + % (helper, sym)) + + @classmethod + def Errors(cls): + return {"templatehelper-import-error": "error", + "templatehelper-no-export": "error", + "templatehelper-nonlist-export": "error", + "templatehelper-nonexistent-export": "error", + "templatehelper-reserved-export": "error", + "templatehelper-underscore-export": "warning"} diff --git a/src/lib/Bcfg2/Server/Lint/Validate.py b/src/lib/Bcfg2/Server/Lint/Validate.py index 09f3f3d25..2042382e7 100644 --- a/src/lib/Bcfg2/Server/Lint/Validate.py +++ b/src/lib/Bcfg2/Server/Lint/Validate.py @@ -6,14 +6,21 @@ import sys import glob import fnmatch import lxml.etree -from subprocess import Popen, PIPE, STDOUT +import Bcfg2.Options import Bcfg2.Server.Lint +from Bcfg2.Utils import Executor class Validate(Bcfg2.Server.Lint.ServerlessPlugin): """ Ensure that all XML files in the Bcfg2 repository validate according to their respective schemas. """ + options = Bcfg2.Server.Lint.ServerlessPlugin.options + [ + Bcfg2.Options.PathOption( + "--schema", cf=("Validate", "schema"), + default="/usr/share/bcfg2/schema", + help="The full path to the XML schema files")] + def __init__(self, *args, **kwargs): Bcfg2.Server.Lint.ServerlessPlugin.__init__(self, *args, **kwargs) @@ -32,14 +39,14 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): "Cfg/**/pubkey.xml": "pubkey.xsd", "Cfg/**/authorizedkeys.xml": "authorizedkeys.xsd", "Cfg/**/authorized_keys.xml": "authorizedkeys.xsd", + "Cfg/**/sslcert.xml": "sslca-cert.xsd", + "Cfg/**/sslkey.xml": "sslca-key.xsd", "SSHbase/**/info.xml": "info.xsd", - "SSLCA/**/info.xml": "info.xsd", "TGenshi/**/info.xml": "info.xsd", "TCheetah/**/info.xml": "info.xsd", "Bundler/*.xml": "bundle.xsd", "Bundler/*.genshi": "bundle.xsd", "Pkgmgr/*.xml": "pkglist.xsd", - "Base/*.xml": "base.xsd", "Rules/*.xml": "rules.xsd", "Defaults/*.xml": "defaults.xsd", "etc/report-configuration.xml": "report-configuration.xsd", @@ -49,16 +56,14 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): "GroupPatterns/config.xml": "grouppatterns.xsd", "NagiosGen/config.xml": "nagiosgen.xsd", "FileProbes/config.xml": "fileprobes.xsd", - "SSLCA/**/cert.xml": "sslca-cert.xsd", - "SSLCA/**/key.xml": "sslca-key.xsd", "GroupLogic/groups.xml": "grouplogic.xsd" } self.filelists = {} self.get_filelists() + self.cmd = Executor() def Run(self): - schemadir = self.config['schema'] for path, schemaname in self.filesets.items(): try: @@ -68,7 +73,8 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): if filelist: # avoid loading schemas for empty file lists - schemafile = os.path.join(schemadir, schemaname) + schemafile = os.path.join(Bcfg2.Options.setup.schema, + schemaname) schema = self._load_schema(schemafile) if schema: for filename in filelist: @@ -108,11 +114,10 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): try: return lxml.etree.parse(filename) except SyntaxError: - lint = Popen(["xmllint", filename], stdout=PIPE, stderr=STDOUT) + result = self.cmd.run(["xmllint", filename]) self.LintError("xml-failed-to-parse", - "%s fails to parse:\n%s" % (filename, - lint.communicate()[0])) - lint.wait() + "%s fails to parse:\n%s" % + (filename, result.stdout + result.stderr)) return False except IOError: self.LintError("xml-failed-to-read", @@ -145,14 +150,11 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): if self.files is None: cmd.append("--xinclude") cmd.extend(["--noout", "--schema", schemafile, filename]) - lint = Popen(cmd, stdout=PIPE, stderr=STDOUT) - output = lint.communicate()[0] - # py3k fix - if not isinstance(output, str): - output = output.decode('utf-8') - if lint.wait(): + result = self.cmd.run(cmd) + if not result.success: self.LintError("xml-failed-to-verify", - "%s fails to verify:\n%s" % (filename, output)) + "%s fails to verify:\n%s" % + (filename, result.stdout + result.stderr)) return False return True @@ -169,8 +171,8 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): listfiles = lambda p: fnmatch.filter(self.files, os.path.join('*', p)) else: - listfiles = lambda p: glob.glob(os.path.join(self.config['repo'], - p)) + listfiles = lambda p: \ + glob.glob(os.path.join(Bcfg2.Options.setup.repository, p)) for path in self.filesets.keys(): if '/**/' in path: @@ -179,9 +181,9 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): else: # self.files is None fpath, fname = path.split('/**/') self.filelists[path] = [] - for root, _, files in \ - os.walk(os.path.join(self.config['repo'], - fpath)): + for root, _, files in os.walk( + os.path.join(Bcfg2.Options.setup.repository, + fpath)): self.filelists[path].extend([os.path.join(root, f) for f in files if f == fname]) diff --git a/src/lib/Bcfg2/Server/Lint/__init__.py b/src/lib/Bcfg2/Server/Lint/__init__.py index 28644263f..f918ad851 100644 --- a/src/lib/Bcfg2/Server/Lint/__init__.py +++ b/src/lib/Bcfg2/Server/Lint/__init__.py @@ -2,16 +2,17 @@ import os import sys +import time +import copy +import fcntl +import struct +import termios import logging -from copy import copy import textwrap import lxml.etree -import fcntl -import termios -import struct -from Bcfg2.Compat import walk_packages - -plugins = [m[1] for m in walk_packages(path=__path__)] # pylint: disable=C0103 +import Bcfg2.Options +import Bcfg2.Server.Core +import Bcfg2.Server.Plugins def _ioctl_GWINSZ(fd): # pylint: disable=C0103 @@ -46,10 +47,10 @@ def get_termsize(): class Plugin(object): """ Base class for all bcfg2-lint plugins """ - def __init__(self, config, errorhandler=None, files=None): + options = [Bcfg2.Options.Common.repository] + + def __init__(self, errorhandler=None, files=None): """ - :param config: A :mod:`Bcfg2.Options` setup dict - :type config: dict :param errorhandler: A :class:`Bcfg2.Server.Lint.ErrorHandler` that will be used to handle lint errors. If one is not provided, a new one will be @@ -63,9 +64,6 @@ class Plugin(object): #: The list of files that bcfg2-lint should be run against self.files = files - #: The Bcfg2.Options setup dict - self.config = config - self.logger = logging.getLogger('bcfg2-lint') if errorhandler is None: #: The error handler @@ -96,9 +94,10 @@ class Plugin(object): False otherwise. """ return (self.files is None or fname in self.files or - os.path.join(self.config['repo'], fname) in self.files or + os.path.join(Bcfg2.Options.setup.repository, + fname) in self.files or os.path.abspath(fname) in self.files or - os.path.abspath(os.path.join(self.config['repo'], + os.path.abspath(os.path.join(Bcfg2.Options.setup.repository, fname)) in self.files) def LintError(self, err, msg): @@ -125,7 +124,7 @@ class Plugin(object): """ xml = None if len(element) or element.text: - el = copy(element) + el = copy.copy(element) if el.text and not keep_text: el.text = '...' for child in el.iterchildren(): @@ -145,8 +144,8 @@ class ErrorHandler(object): def __init__(self, errors=None): """ - :param config: An initial dict of errors to register - :type config: dict + :param errors: An initial dict of errors to register + :type errors: dict """ #: The number of errors passed to this error handler self.errors = 0 @@ -267,12 +266,10 @@ class ServerPlugin(Plugin): # pylint: disable=W0223 """ Base class for bcfg2-lint plugins that check things that require the running Bcfg2 server. """ - def __init__(self, core, config, errorhandler=None, files=None): + def __init__(self, core, errorhandler=None, files=None): """ :param core: The Bcfg2 server core :type core: Bcfg2.Server.Core.BaseCore - :param config: A :mod:`Bcfg2.Options` setup dict - :type config: dict :param errorhandler: A :class:`Bcfg2.Server.Lint.ErrorHandler` that will be used to handle lint errors. If one is not provided, a new one will be @@ -282,7 +279,7 @@ class ServerPlugin(Plugin): # pylint: disable=W0223 the bcfg2-lint ``--stdin`` option.) :type files: list of strings """ - Plugin.__init__(self, config, errorhandler=errorhandler, files=files) + Plugin.__init__(self, errorhandler=errorhandler, files=files) #: The server core self.core = core @@ -290,3 +287,167 @@ class ServerPlugin(Plugin): # pylint: disable=W0223 #: The metadata plugin self.metadata = self.core.metadata + + +class LintPluginAction(Bcfg2.Options.ComponentAction): + """ We want to load all lint plugins that pertain to server + plugins. In order to do this, we hijack the __call__() method of + this action and add all of the server plugins on the fly """ + + bases = ['Bcfg2.Server.Lint'] + + def __call__(self, parser, namespace, values, option_string=None): + for plugin in getattr(Bcfg2.Options.setup, "plugins", []): + module = sys.modules[plugin.__module__] + if hasattr(module, "%sLint" % plugin.name): + print("Adding lint plugin %s" % plugin) + values.append(plugin) + Bcfg2.Options.ComponentAction.__call__(self, parser, namespace, values, + option_string) + + +class CLI(object): + """ The bcfg2-lint CLI """ + options = Bcfg2.Server.Core.Core.options + [ + Bcfg2.Options.PathOption( + '--lint-config', default='/etc/bcfg2-lint.conf', + action=Bcfg2.Options.ConfigFileAction, + help='Specify bcfg2-lint configuration file'), + Bcfg2.Options.Option( + "--lint-plugins", cf=('lint', 'plugins'), + type=Bcfg2.Options.Types.comma_list, action=LintPluginAction, + help='bcfg2-lint plugin list'), + Bcfg2.Options.BooleanOption( + '--list-errors', help='Show error handling'), + Bcfg2.Options.BooleanOption( + '--stdin', help='Operate on a list of files supplied on stdin'), + Bcfg2.Options.Option( + cf=("errors", '*'), dest="lint_errors", + help="How to handle bcfg2-lint errors")] + + def __init__(self): + parser = Bcfg2.Options.get_parser( + description="Manage a running Bcfg2 server", + components=[self]) + parser.parse() + + self.logger = logging.getLogger(parser.prog) + + # automatically add Lint plugins for loaded server plugins + for plugin in Bcfg2.Options.setup.plugins: + try: + Bcfg2.Options.setup.lint_plugins.append( + getattr( + __import__("Bcfg2.Server.Lint.%s" % plugin.__name__, + fromlist=[plugin.__name__]), + plugin.__name__)) + self.logger.debug("Automatically adding lint plugin %s" % + plugin.__name__) + except ImportError: + # no lint plugin for this server plugin + self.logger.debug("No lint plugin for %s" % plugin.__name__) + except AttributeError: + self.logger.error("Failed to load plugin %s: %s" % + (plugin.__name__, sys.exc_info()[1])) + + self.logger.debug("Running lint with plugins: %s" % + [p.__name__ + for p in Bcfg2.Options.setup.lint_plugins]) + + if Bcfg2.Options.setup.stdin: + self.files = [s.strip() for s in sys.stdin.readlines()] + else: + self.files = None + self.errorhandler = self.get_errorhandler() + self.serverlessplugins = [] + self.serverplugins = [] + for plugin in Bcfg2.Options.setup.lint_plugins: + if issubclass(plugin, ServerPlugin): + self.serverplugins.append(plugin) + else: + self.serverlessplugins.append(plugin) + + def run(self): + """ Run bcfg2-lint """ + if Bcfg2.Options.setup.list_errors: + for plugin in self.serverplugins + self.serverlessplugins: + self.errorhandler.RegisterErrors(getattr(plugin, 'Errors')()) + + print("%-35s %-35s" % ("Error name", "Handler")) + for err, handler in self.errorhandler.errortypes.items(): + print("%-35s %-35s" % (err, handler.__name__)) + return 0 + + if not self.serverplugins and not self.serverlessplugins: + self.logger.error("No lint plugins loaded!") + return 1 + + self.run_serverless_plugins() + + if self.serverplugins: + if self.errorhandler.errors: + # it would be swell if we could try to start the server + # even if there were errors with the serverless plugins, + # but since XML parsing errors occur in the FAM thread + # (not in the core server thread), there's no way we can + # start the server and try to catch exceptions -- + # bcfg2-lint isn't in the same stack as the exceptions. + # so we're forced to assume that a serverless plugin error + # will prevent the server from starting + print("Serverless plugins encountered errors, skipping server " + "plugins") + else: + self.run_server_plugins() + + if (self.errorhandler.errors or + self.errorhandler.warnings or + Bcfg2.Options.setup.verbose): + print("%d errors" % self.errorhandler.errors) + print("%d warnings" % self.errorhandler.warnings) + + if self.errorhandler.errors: + return 2 + elif self.errorhandler.warnings: + return 3 + else: + return 0 + + def get_errorhandler(self): + """ get a Bcfg2.Server.Lint.ErrorHandler object """ + return Bcfg2.Server.Lint.ErrorHandler( + errors=Bcfg2.Options.setup.lint_errors) + + def run_serverless_plugins(self): + """ Run serverless plugins """ + self.logger.debug("Running serverless plugins: %s" % + [p.__name__ for p in self.serverlessplugins]) + for plugin in self.serverlessplugins: + self.logger.debug(" Running %s" % plugin.__name__) + plugin(files=self.files, errorhandler=self.errorhandler).Run() + + def run_server_plugins(self): + """ run plugins that require a running server to run """ + core = Bcfg2.Server.Core.Core() + core.load_plugins() + core.block_for_fam_events(handle_events=True) + try: + self.logger.debug("Running server plugins: %s" % + [p.__name__ for p in self.serverplugins]) + for plugin in self.serverplugins: + self.logger.debug(" Running %s" % plugin.__name__) + plugin(core, + files=self.files, errorhandler=self.errorhandler).Run() + finally: + core.shutdown() + + def _run_plugin(self, plugin, args=None): + """ Run a single bcfg2-lint plugin """ + if args is None: + args = [] + start = time.time() + # python 2.5 doesn't support mixing *magic and keyword arguments + kwargs = dict(files=self.files, errorhandler=self.errorhandler) + rv = plugin(*args, **kwargs).Run() + self.logger.debug(" Ran %s in %0.2f seconds" % (plugin.__name__, + time.time() - start)) + return rv |