summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Server/Lint
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Bcfg2/Server/Lint')
-rw-r--r--src/lib/Bcfg2/Server/Lint/AWSTags.py33
-rw-r--r--src/lib/Bcfg2/Server/Lint/Bundler.py59
-rw-r--r--src/lib/Bcfg2/Server/Lint/Cfg.py118
-rw-r--r--src/lib/Bcfg2/Server/Lint/Comments.py116
-rw-r--r--src/lib/Bcfg2/Server/Lint/Crypto.py61
-rw-r--r--[-rwxr-xr-x]src/lib/Bcfg2/Server/Lint/Genshi.py62
-rw-r--r--src/lib/Bcfg2/Server/Lint/GroupNames.py14
-rw-r--r--src/lib/Bcfg2/Server/Lint/GroupPatterns.py44
-rw-r--r--src/lib/Bcfg2/Server/Lint/InfoXML.py30
-rw-r--r--src/lib/Bcfg2/Server/Lint/Jinja2.py41
-rw-r--r--src/lib/Bcfg2/Server/Lint/MergeFiles.py103
-rw-r--r--src/lib/Bcfg2/Server/Lint/Metadata.py172
-rw-r--r--src/lib/Bcfg2/Server/Lint/Pkgmgr.py50
-rw-r--r--src/lib/Bcfg2/Server/Lint/RequiredAttrs.py44
-rw-r--r--src/lib/Bcfg2/Server/Lint/TemplateAbuse.py17
-rw-r--r--src/lib/Bcfg2/Server/Lint/TemplateHelper.py97
-rw-r--r--src/lib/Bcfg2/Server/Lint/Validate.py44
-rw-r--r--src/lib/Bcfg2/Server/Lint/ValidateJSON.py6
-rw-r--r--src/lib/Bcfg2/Server/Lint/__init__.py225
19 files changed, 1146 insertions, 190 deletions
diff --git a/src/lib/Bcfg2/Server/Lint/AWSTags.py b/src/lib/Bcfg2/Server/Lint/AWSTags.py
new file mode 100644
index 000000000..c6d7a3a30
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Lint/AWSTags.py
@@ -0,0 +1,33 @@
+""" ``bcfg2-lint`` plugin to check all given :ref:`AWSTags
+<server-plugins-connectors-awstags>` patterns for validity."""
+
+import re
+import sys
+import Bcfg2.Server.Lint
+
+
+class AWSTags(Bcfg2.Server.Lint.ServerPlugin):
+ """ ``bcfg2-lint`` plugin to check all given :ref:`AWSTags
+ <server-plugins-connectors-awstags>` patterns for validity. """
+ __serverplugin__ = 'AWSTags'
+
+ def Run(self):
+ cfg = self.core.plugins['AWSTags'].config
+ for entry in cfg.xdata.xpath('//Tag'):
+ self.check(entry, "name")
+ if entry.get("value"):
+ self.check(entry, "value")
+
+ @classmethod
+ def Errors(cls):
+ return {"pattern-fails-to-initialize": "error"}
+
+ def check(self, entry, attr):
+ """ Check a single attribute (``name`` or ``value``) of a
+ single entry for validity. """
+ try:
+ re.compile(entry.get(attr))
+ except re.error:
+ self.LintError("pattern-fails-to-initialize",
+ "'%s' regex could not be compiled: %s\n %s" %
+ (attr, sys.exc_info()[1], entry.get("name")))
diff --git a/src/lib/Bcfg2/Server/Lint/Bundler.py b/src/lib/Bcfg2/Server/Lint/Bundler.py
new file mode 100644
index 000000000..576e157ad
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Lint/Bundler.py
@@ -0,0 +1,59 @@
+""" ``bcfg2-lint`` plugin for :ref:`Bundler
+<server-plugins-structures-bundler>` """
+
+from Bcfg2.Server.Lint import ServerPlugin
+
+
+class Bundler(ServerPlugin):
+ """ Perform various :ref:`Bundler
+ <server-plugins-structures-bundler>` checks. """
+ __serverplugin__ = 'Bundler'
+
+ 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..13b04a6b8
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Lint/Cfg.py
@@ -0,0 +1,118 @@
+""" ``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 """
+ __serverplugin__ = 'Cfg'
+
+ 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..fbe84de87 100644
--- a/src/lib/Bcfg2/Server/Lint/Comments.py
+++ b/src/lib/Bcfg2/Server/Lint/Comments.py
@@ -2,12 +2,14 @@
import os
import lxml.etree
+import Bcfg2.Options
import Bcfg2.Server.Lint
from Bcfg2.Server import XI_NAMESPACE
from Bcfg2.Server.Plugins.Cfg.CfgPlaintextGenerator \
import CfgPlaintextGenerator
from Bcfg2.Server.Plugins.Cfg.CfgGenshiGenerator import CfgGenshiGenerator
from Bcfg2.Server.Plugins.Cfg.CfgCheetahGenerator import CfgCheetahGenerator
+from Bcfg2.Server.Plugins.Cfg.CfgJinja2Generator import CfgJinja2Generator
from Bcfg2.Server.Plugins.Cfg.CfgInfoXML import CfgInfoXML
@@ -16,6 +18,97 @@ 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", "jinja2_keywords"),
+ type=Bcfg2.Options.Types.comma_list, default=[],
+ help="Required keywords for Jinja2-templated Cfg files"),
+ Bcfg2.Options.Option(
+ cf=("Comments", "jinja2_comments"),
+ type=Bcfg2.Options.Types.comma_list, default=[],
+ help="Required comments for Jinja2-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", "probes_keywords"),
+ type=Bcfg2.Options.Types.comma_list, default=[],
+ help="Required keywords for probes"),
+ Bcfg2.Options.Option(
+ cf=("Comments", "probes_comments"),
+ type=Bcfg2.Options.Types.comma_list, default=[],
+ help="Required comments for probes"),
+ Bcfg2.Options.Option(
+ cf=("Comments", "metadata_keywords"),
+ type=Bcfg2.Options.Types.comma_list, default=[],
+ help="Required keywords for metadata files"),
+ Bcfg2.Options.Option(
+ cf=("Comments", "metadata_comments"),
+ type=Bcfg2.Options.Types.comma_list, default=[],
+ help="Required comments for metadata files")]
+
def __init__(self, *args, **kwargs):
Bcfg2.Server.Lint.ServerPlugin.__init__(self, *args, **kwargs)
self.config_cache = {}
@@ -73,17 +166,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]
@@ -162,9 +252,11 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin):
rtype = "cfg"
elif isinstance(entry, CfgCheetahGenerator):
rtype = "cheetah"
+ elif isinstance(entry, CfgJinja2Generator):
+ rtype = "jinja2"
elif isinstance(entry, CfgInfoXML):
self.check_xml(entry.infoxml.name,
- entry.infoxml.pnode.data,
+ entry.infoxml.xdata,
"infoxml")
continue
if rtype:
diff --git a/src/lib/Bcfg2/Server/Lint/Crypto.py b/src/lib/Bcfg2/Server/Lint/Crypto.py
new file mode 100644
index 000000000..53a54031c
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Lint/Crypto.py
@@ -0,0 +1,61 @@
+""" Check for data that claims to be encrypted, but is not. """
+
+import os
+import lxml.etree
+import Bcfg2.Options
+from Bcfg2.Server.Lint import ServerlessPlugin
+from Bcfg2.Server.Encryption import is_encrypted
+
+
+class Crypto(ServerlessPlugin):
+ """ Check for templated scripts or executables. """
+
+ def Run(self):
+ if os.path.exists(os.path.join(Bcfg2.Options.setup.repository, "Cfg")):
+ self.check_cfg()
+ if os.path.exists(os.path.join(Bcfg2.Options.setup.repository,
+ "Properties")):
+ self.check_properties()
+ # TODO: check all XML files
+
+ @classmethod
+ def Errors(cls):
+ return {"unencrypted-cfg": "error",
+ "empty-encrypted-properties": "error",
+ "unencrypted-properties": "error"}
+
+ def check_cfg(self):
+ """ Check for Cfg files that end in .crypt but aren't encrypted """
+ for root, _, files in os.walk(
+ os.path.join(Bcfg2.Options.setup.repository, "Cfg")):
+ for fname in files:
+ fpath = os.path.join(root, fname)
+ if self.HandlesFile(fpath) and fname.endswith(".crypt"):
+ if not is_encrypted(open(fpath).read()):
+ self.LintError(
+ "unencrypted-cfg",
+ "%s is a .crypt file, but it is not encrypted" %
+ fpath)
+
+ def check_properties(self):
+ """ Check for Properties data that has an ``encrypted`` attribute but
+ aren't encrypted """
+ for root, _, files in os.walk(
+ os.path.join(Bcfg2.Options.setup.repository, "Properties")):
+ for fname in files:
+ fpath = os.path.join(root, fname)
+ if self.HandlesFile(fpath) and fname.endswith(".xml"):
+ xdata = lxml.etree.parse(fpath)
+ for elt in xdata.xpath('//*[@encrypted]'):
+ if not elt.text:
+ self.LintError(
+ "empty-encrypted-properties",
+ "Element in %s has an 'encrypted' attribute, "
+ "but no text content: %s" %
+ (fpath, self.RenderXML(elt)))
+ elif not is_encrypted(elt.text):
+ self.LintError(
+ "unencrypted-properties",
+ "Element in %s has an 'encrypted' attribute, "
+ "but is not encrypted: %s" %
+ (fpath, self.RenderXML(elt)))
diff --git a/src/lib/Bcfg2/Server/Lint/Genshi.py b/src/lib/Bcfg2/Server/Lint/Genshi.py
index 1ecb6da42..a2581e70b 100755..100644
--- 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..e9813b7e9
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Lint/GroupPatterns.py
@@ -0,0 +1,44 @@
+""" ``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, \
+ PatternInitializationError
+
+
+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."""
+ __serverplugin__ = 'GroupPatterns'
+
+ 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 """
+ for el in entry.findall(ptype):
+ pat = el.text
+ try:
+ if ptype == "NamePattern":
+ PatternMap(pat, None, groups)
+ else:
+ PatternMap(None, pat, groups)
+ except PatternInitializationError:
+ 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..950a86f01 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,16 @@ class InfoXML(Bcfg2.Server.Lint.ServerPlugin):
* Paranoid mode disabled in an ``info.xml`` file;
* Required attributes missing from ``info.xml``
"""
+ __serverplugin__ = 'Cfg'
+
+ 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 +36,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 +52,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 +61,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/Jinja2.py b/src/lib/Bcfg2/Server/Lint/Jinja2.py
new file mode 100644
index 000000000..333249cc2
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Lint/Jinja2.py
@@ -0,0 +1,41 @@
+""" Check Jinja2 templates for syntax errors. """
+
+import sys
+import Bcfg2.Server.Lint
+from jinja2 import Template, TemplateSyntaxError
+from Bcfg2.Server.Plugins.Cfg.CfgJinja2Generator import CfgJinja2Generator
+
+
+class Jinja2(Bcfg2.Server.Lint.ServerPlugin):
+ """ Check Jinja2 templates for syntax errors. """
+
+ def Run(self):
+ if 'Cfg' in self.core.plugins:
+ self.check_cfg()
+
+ @classmethod
+ def Errors(cls):
+ return {"jinja2-syntax-error": "error",
+ "unknown-jinja2-error": "error"}
+
+ def check_template(self, entry):
+ """ Generic check for all jinja2 templates """
+ try:
+ Template(entry.data.decode(entry.encoding))
+ except TemplateSyntaxError:
+ err = sys.exc_info()[1]
+ self.LintError("jinja2-syntax-error",
+ "Jinja2 syntax error in %s: %s" % (entry.name, err))
+ except:
+ err = sys.exc_info()[1]
+ self.LintError("unknown-jinja2-error",
+ "Unknown Jinja2 error in %s: %s" % (entry.name,
+ err))
+
+ def check_cfg(self):
+ """ Check jinja2 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, CfgJinja2Generator)):
+ self.check_template(entry)
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..e445892d1
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Lint/Metadata.py
@@ -0,0 +1,172 @@
+""" ``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.
+ """
+ __serverplugin__ = 'Metadata'
+
+ 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..eed6d4c19
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Lint/Pkgmgr.py
@@ -0,0 +1,50 @@
+""" ``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. """
+ __serverplugin__ = 'Pkgmgr'
+
+ 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 ce8b237b9..ebf4c4954 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
@@ -162,7 +156,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 "
@@ -176,7 +170,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:"
@@ -191,32 +185,34 @@ 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
+ """ Check bundles for BoundPath and BoundPackage entries with missing
attrs. """
if 'Bundler' not in self.core.plugins:
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)
+ # ensure that abstract Path tags have either name
+ # or glob specified
+ for path in bundle.xdata.xpath("//Path"):
+ if ('name' not in path.attrib and
+ 'glob' not in path.attrib):
+ self.LintError(
+ "required-attrs-missing",
+ "Path tags require either a 'name' or 'glob' "
+ "attribute: \n%s" % self.RenderXML(path))
# ensure that abstract Package tags have either name
# or group specified
- for package in xdata.xpath("//Package"):
+ for package in bundle.xdata.xpath("//Package"):
if ('name' not in package.attrib and
'group' not in package.attrib):
self.LintError(
@@ -272,7 +268,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/TemplateAbuse.py b/src/lib/Bcfg2/Server/Lint/TemplateAbuse.py
index fca9d14a9..a437c1318 100644
--- a/src/lib/Bcfg2/Server/Lint/TemplateAbuse.py
+++ b/src/lib/Bcfg2/Server/Lint/TemplateAbuse.py
@@ -4,20 +4,24 @@ import os
import stat
import Bcfg2.Server.Lint
from Bcfg2.Compat import any # pylint: disable=W0622
-from Bcfg2.Server.Plugin import DEFAULT_FILE_METADATA
+from Bcfg2.Server.Plugin import default_path_metadata
from Bcfg2.Server.Plugins.Cfg.CfgInfoXML import CfgInfoXML
from Bcfg2.Server.Plugins.Cfg.CfgGenshiGenerator import CfgGenshiGenerator
from Bcfg2.Server.Plugins.Cfg.CfgCheetahGenerator import CfgCheetahGenerator
+from Bcfg2.Server.Plugins.Cfg.CfgJinja2Generator import CfgJinja2Generator
from Bcfg2.Server.Plugins.Cfg.CfgEncryptedGenshiGenerator import \
CfgEncryptedGenshiGenerator
from Bcfg2.Server.Plugins.Cfg.CfgEncryptedCheetahGenerator import \
CfgEncryptedCheetahGenerator
+from Bcfg2.Server.Plugins.Cfg.CfgEncryptedJinja2Generator import \
+ CfgEncryptedJinja2Generator
class TemplateAbuse(Bcfg2.Server.Lint.ServerPlugin):
""" Check for templated scripts or executables. """
- templates = [CfgGenshiGenerator, CfgCheetahGenerator,
- CfgEncryptedGenshiGenerator, CfgEncryptedCheetahGenerator]
+ templates = [CfgGenshiGenerator, CfgCheetahGenerator, CfgJinja2Generator,
+ CfgEncryptedGenshiGenerator, CfgEncryptedCheetahGenerator,
+ CfgEncryptedJinja2Generator]
extensions = [".pl", ".py", ".sh", ".rb"]
def Run(self):
@@ -58,10 +62,11 @@ class TemplateAbuse(Bcfg2.Server.Lint.ServerPlugin):
# finally, check for executable permissions in info.xml
for entry in entryset.entries.values():
if isinstance(entry, CfgInfoXML):
- for pinfo in entry.infoxml.pnode.data.xpath("//FileInfo"):
+ for pinfo in entry.infoxml.xdata.xpath("//FileInfo/Info"):
try:
- mode = int(pinfo.get("mode",
- DEFAULT_FILE_METADATA['mode']), 8)
+ mode = int(
+ pinfo.get("mode",
+ default_path_metadata()['mode']), 8)
except ValueError:
# LintError will be produced by RequiredAttrs plugin
self.logger.warning("Non-octal mode: %s" % mode)
diff --git a/src/lib/Bcfg2/Server/Lint/TemplateHelper.py b/src/lib/Bcfg2/Server/Lint/TemplateHelper.py
new file mode 100644
index 000000000..9d05516f1
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Lint/TemplateHelper.py
@@ -0,0 +1,97 @@
+""" ``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.
+ """
+ __serverplugin__ = 'TemplateHelper'
+
+ def __init__(self, *args, **kwargs):
+ ServerPlugin.__init__(self, *args, **kwargs)
+ # we instantiate a dummy helper to discover which keywords and
+ # defaults are reserved
+ dummy = HelperModule("foo.py")
+ self.reserved_keywords = dir(dummy)
+ self.reserved_defaults = dummy.reserved_defaults
+
+ 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))
+ if sym in getattr(module, "__default__", []):
+ self.LintError("templatehelper-export-and-default",
+ "%s: %s is listed in both __default__ and "
+ "__export__" % (helper, sym))
+
+ for sym in getattr(module, "__default__", []):
+ if sym in self.reserved_defaults:
+ self.LintError("templatehelper-reserved-default",
+ "%s: default symbol %s is reserved" %
+ (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-reserved-default": "error",
+ "templatehelper-underscore-export": "warning",
+ "templatehelper-export-and-default": "warning"}
diff --git a/src/lib/Bcfg2/Server/Lint/Validate.py b/src/lib/Bcfg2/Server/Lint/Validate.py
index 1e33ec398..d6f18afbb 100644
--- a/src/lib/Bcfg2/Server/Lint/Validate.py
+++ b/src/lib/Bcfg2/Server/Lint/Validate.py
@@ -9,15 +9,22 @@ import os
import sys
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/schemas",
+ help="The full path to the XML schema files")]
+
def __init__(self, *args, **kwargs):
Bcfg2.Server.Lint.ServerlessPlugin.__init__(self, *args, **kwargs)
@@ -36,14 +43,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",
@@ -54,16 +61,14 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
"AWSTags/config.xml": "awstags.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:
@@ -73,7 +78,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:
@@ -122,11 +128,10 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
if self.files is None:
cmd.append("--xinclude")
cmd.append(filename)
- lint = Popen(cmd, stdout=PIPE, stderr=STDOUT)
+ result = self.cmd.run(cmd)
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",
@@ -188,14 +193,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
@@ -215,9 +217,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/ValidateJSON.py b/src/lib/Bcfg2/Server/Lint/ValidateJSON.py
index bdbe6a271..f7cf5d549 100644
--- a/src/lib/Bcfg2/Server/Lint/ValidateJSON.py
+++ b/src/lib/Bcfg2/Server/Lint/ValidateJSON.py
@@ -57,9 +57,9 @@ class ValidateJSON(Bcfg2.Server.Lint.ServerlessPlugin):
rv.extend(self.list_matching_files(path))
else: # self.files is None
fpath, fname = path.split('/**/')
- 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)):
rv.extend([os.path.join(root, f)
for f in files if f == fname])
else:
diff --git a/src/lib/Bcfg2/Server/Lint/__init__.py b/src/lib/Bcfg2/Server/Lint/__init__.py
index ae2b81a61..873e5f149 100644
--- a/src/lib/Bcfg2/Server/Lint/__init__.py
+++ b/src/lib/Bcfg2/Server/Lint/__init__.py
@@ -1,22 +1,25 @@
""" Base classes for Lint plugins and error handling """
-import os
+import copy
+import fcntl
import fnmatch
import glob
-import sys
import logging
-from copy import copy
+import os
+import struct
+import sys
+import termios
import textwrap
import time
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
+from Bcfg2.Compat import walk_packages
+from Bcfg2.Options import _debug
def _ioctl_GWINSZ(fd): # pylint: disable=C0103
@@ -24,7 +27,7 @@ def _ioctl_GWINSZ(fd): # pylint: disable=C0103
from the given file descriptor """
try:
return struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'))
- except: # pylint: disable=W0702
+ except (IOError, struct.error):
return None
@@ -38,7 +41,7 @@ def get_termsize():
fd = os.open(os.ctermid(), os.O_RDONLY)
dims = _ioctl_GWINSZ(fd)
os.close(fd)
- except: # pylint: disable=W0702
+ except IOError:
pass
if not dims:
try:
@@ -51,10 +54,15 @@ def get_termsize():
class Plugin(object):
""" Base class for all bcfg2-lint plugins """
- def __init__(self, config, errorhandler=None, files=None):
+ #: Name of the matching server plugin or None if there is no
+ #: matching one. If this is None the lint plugin will only loaded
+ #: by default if the matching server plugin is enabled, too.
+ __serverplugin__ = 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
@@ -68,9 +76,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
@@ -101,9 +106,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):
@@ -130,7 +136,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():
@@ -149,7 +155,8 @@ class Plugin(object):
if self.files is not None:
return fnmatch.filter(self.files, os.path.join('*', path))
else:
- return glob.glob(os.path.join(self.config['repo'], path))
+ return glob.glob(os.path.join(Bcfg2.Options.setup.repository,
+ path))
class ErrorHandler(object):
@@ -157,8 +164,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
@@ -279,12 +286,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
@@ -294,7 +299,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
@@ -302,3 +307,171 @@ class ServerPlugin(Plugin): # pylint: disable=W0223
#: The metadata plugin
self.metadata = self.core.metadata
+
+
+class LintPluginAction(Bcfg2.Options.ComponentAction):
+ """ Option parser action to load lint plugins """
+ bases = ['Bcfg2.Server.Lint']
+
+
+class LintPluginOption(Bcfg2.Options.Option):
+ """ Option class for the lint_plugins """
+
+ def early_parsing_hook(self, namespace):
+ """
+ We want a usefull default for the enabled lint plugins.
+ Therfore we use all importable plugins, that either pertain
+ with enabled server plugins or that has no matching plugin.
+ """
+
+ plugins = [p.__name__ for p in namespace.plugins]
+ for loader, name, _is_pkg in walk_packages(path=__path__):
+ try:
+ module = loader.find_module(name).load_module(name)
+ plugin = getattr(module, name)
+ if plugin.__serverplugin__ is None or \
+ plugin.__serverplugin__ in plugins:
+ _debug("Automatically adding lint plugin %s" %
+ plugin.__name__)
+ self.default.append(plugin.__name__)
+ except ImportError:
+ pass
+
+
+class _EarlyOptions(object):
+ """ We need the server.plugins options in an early parsing hook
+ for determining the default value for the lint_plugins. So we
+ create a component that is parsed before the other options. """
+
+ parse_first = True
+ options = [Bcfg2.Options.Common.plugins]
+
+
+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'),
+ LintPluginOption(
+ "--lint-plugins", cf=('lint', 'plugins'), default=[],
+ 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, _EarlyOptions])
+ parser.parse()
+
+ self.logger = logging.getLogger(parser.prog)
+
+ 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()
+ try:
+ core.load_plugins()
+ core.block_for_fam_events(handle_events=True)
+ 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