summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChris St. Pierre <chris.a.st.pierre@gmail.com>2013-06-27 10:40:19 -0400
committerChris St. Pierre <chris.a.st.pierre@gmail.com>2013-06-27 10:40:19 -0400
commit2d1f13115150af2dd9b74e1a928f40fc19cf0dd1 (patch)
treece8993bad3ae15ca4f61c5684450de89c6e11ca9
parent67fda2597efe7cec04b037138cef86f1e328cc4c (diff)
downloadbcfg2-2d1f13115150af2dd9b74e1a928f40fc19cf0dd1.tar.gz
bcfg2-2d1f13115150af2dd9b74e1a928f40fc19cf0dd1.tar.bz2
bcfg2-2d1f13115150af2dd9b74e1a928f40fc19cf0dd1.zip
Options: migrated bcfg2-lint to new parser
-rw-r--r--doc/development/lint.txt44
-rw-r--r--src/lib/Bcfg2/Server/Lint/Bundler.py55
-rw-r--r--src/lib/Bcfg2/Server/Lint/Cfg.py76
-rw-r--r--src/lib/Bcfg2/Server/Lint/Comments.py96
-rwxr-xr-xsrc/lib/Bcfg2/Server/Lint/Genshi.py38
-rw-r--r--src/lib/Bcfg2/Server/Lint/GroupNames.py5
-rw-r--r--src/lib/Bcfg2/Server/Lint/GroupPatterns.py40
-rw-r--r--src/lib/Bcfg2/Server/Lint/InfoXML.py18
-rw-r--r--src/lib/Bcfg2/Server/Lint/MergeFiles.py38
-rw-r--r--src/lib/Bcfg2/Server/Lint/Metadata.py148
-rw-r--r--src/lib/Bcfg2/Server/Lint/Pkgmgr.py46
-rw-r--r--src/lib/Bcfg2/Server/Lint/RequiredAttrs.py5
-rw-r--r--src/lib/Bcfg2/Server/Lint/TemplateHelper.py77
-rw-r--r--src/lib/Bcfg2/Server/Lint/Validate.py20
-rw-r--r--src/lib/Bcfg2/Server/Lint/__init__.py204
-rwxr-xr-xsrc/sbin/bcfg2-lint209
l---------src/sbin/bcfg2-repo-validate1
17 files changed, 804 insertions, 316 deletions
diff --git a/doc/development/lint.txt b/doc/development/lint.txt
index 6a4651f92..685823ab1 100644
--- a/doc/development/lint.txt
+++ b/doc/development/lint.txt
@@ -10,14 +10,14 @@
lets you easily write your own plugins to verify various parts of your
Bcfg2 specification.
-Plugins are loaded in one of two ways:
+Plugins are included in a module of the same name as the plugin class
+in :mod:`Bcfg2.Server.Lint`, e.g., :mod:`Bcfg2.Server.Lint.Validate`.
-* They may be included in a module of the same name as the plugin
- class in :mod:`Bcfg2.Server.Lint`, e.g.,
- :mod:`Bcfg2.Server.Lint.Validate`.
-* They may be included directly in a Bcfg2 server plugin, called
- "<plugin>Lint", e.g.,
- :class:`Bcfg2.Server.Plugins.Metadata.MetadataLint`.
+.. note::
+
+ It is no longer possible to include lint plugins directly in a
+ Bcfg2 server plugin, e.g.,
+ :class:`Bcfg2.Server.Plugins.Metadata.MetadataLint`.
Plugin Types
============
@@ -106,10 +106,10 @@ Basics
Existing ``bcfg2-lint`` Plugins
===============================
-BundlerLint
------------
+Bundler
+-------
-.. autoclass:: Bcfg2.Server.Plugins.Bundler.BundlerLint
+.. automodule:: Bcfg2.Server.Lint.Bundler
Comments
--------
@@ -126,10 +126,10 @@ GroupNames
.. automodule:: Bcfg2.Server.Lint.GroupNames
-GroupPatternsLint
------------------
+GroupPatterns
+-------------
-.. autoclass:: Bcfg2.Server.Plugins.GroupPatterns.GroupPatternsLint
+.. automodule:: Bcfg2.Server.Lint.GroupPatterns
InfoXML
-------
@@ -141,25 +141,25 @@ MergeFiles
.. automodule:: Bcfg2.Server.Lint.MergeFiles
-MetadataLint
-------------
+Metadata
+--------
-.. autoclass:: Bcfg2.Server.Plugins.Metadata.MetadataLint
+.. automodule:: Bcfg2.Server.Lint.Metadata
-PkgmgrLint
-----------
+Pkgmgr
+------
-.. autoclass:: Bcfg2.Server.Plugins.Pkgmgr.PkgmgrLint
+.. automodule:: Bcfg2.Server.Lint.Pkgmgr
RequiredAttrs
-------------
.. automodule:: Bcfg2.Server.Lint.RequiredAttrs
-TemplateHelperLint
-------------------
+TemplateHelper
+--------------
-.. autoclass:: Bcfg2.Server.Plugins.TemplateHelper.TemplateHelperLint
+.. automodule:: Bcfg2.Server.Lint.TemplateHelper
Validate
--------
diff --git a/src/lib/Bcfg2/Server/Lint/Bundler.py b/src/lib/Bcfg2/Server/Lint/Bundler.py
new file mode 100644
index 000000000..b41313349
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Lint/Bundler.py
@@ -0,0 +1,55 @@
+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..933e677e0
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Lint/Cfg.py
@@ -0,0 +1,76 @@
+import os
+from Bcfg2.Server.Lint import ServerPlugin
+
+
+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()
+
+ @classmethod
+ def Errors(cls):
+ return {"no-pubkey-xml": "warning",
+ "unknown-cfg-files": "error",
+ "extra-cfg-files": "error"}
+
+ 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 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 = []
+ for hdlr in cfg.handlers:
+ ignore.extend(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):
+ all_files.update(os.path.join(root, fname)
+ for fname in files
+ if not any(fname.endswith("." + i)
+ for i in ignore))
+
+ # 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..c9a34a75f 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,82 @@ 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 +150,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 da8da1aa4..76e1986f9 100755
--- a/src/lib/Bcfg2/Server/Lint/Genshi.py
+++ b/src/lib/Bcfg2/Server/Lint/Genshi.py
@@ -18,7 +18,20 @@ class Genshi(Bcfg2.Server.Lint.ServerPlugin):
@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):
+ 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. """
@@ -27,30 +40,13 @@ class Genshi(Bcfg2.Server.Lint.ServerPlugin):
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))
+ 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
entry.template is not None):
- try:
- loader.load(entry.name, cls=MarkupTemplate)
- except TemplateSyntaxError:
- err = sys.exc_info()[1]
- self.LintError("genshi-syntax-error",
- "Genshi syntax error: %s" % err)
+ 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 730f32750..e28080300 100644
--- a/src/lib/Bcfg2/Server/Lint/GroupNames.py
+++ b/src/lib/Bcfg2/Server/Lint/GroupNames.py
@@ -39,7 +39,8 @@ 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. """
@@ -52,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..8a0ab4f18
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Lint/GroupPatterns.py
@@ -0,0 +1,40 @@
+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 184f657b7..4b1513a11 100644
--- a/src/lib/Bcfg2/Server/Lint/InfoXML.py
+++ b/src/lib/Bcfg2/Server/Lint/InfoXML.py
@@ -15,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
@@ -26,7 +35,7 @@ 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",
@@ -42,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:
@@ -52,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..dff95fbf3 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") """
+ threshold = float(val)
+ if threshold > 1:
+ threshold /= 100
+ return threshold
+
+
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()
@@ -48,19 +63,10 @@ class MergeFiles(Bcfg2.Server.Lint.ServerPlugin):
""" 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 = []
elist = list(entries.items())
while elist:
- result = self._find_similar(elist.pop(0), copy.copy(elist),
- threshold)
+ result = self._find_similar(elist.pop(0), copy.copy(elist))
if len(result) > 1:
elist = [(fname, fdata)
for fname, fdata in elist
@@ -68,7 +74,7 @@ class MergeFiles(Bcfg2.Server.Lint.ServerPlugin):
rv.append(result)
return rv
- 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
@@ -80,9 +86,9 @@ class MergeFiles(Bcfg2.Server.Lint.ServerPlugin):
cname, cdata = others.pop(0)
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))
+ if (seqmatch.real_quick_ratio() > Bcfg2.Options.setup.threshold and
+ seqmatch.quick_ratio() > Bcfg2.Options.setup.threshold and
+ seqmatch.ratio() > Bcfg2.Options.setup.threshold):
+ rv.extend(
+ self._find_similar((cname, cdata), copy.copy(others)))
return rv
diff --git a/src/lib/Bcfg2/Server/Lint/Metadata.py b/src/lib/Bcfg2/Server/Lint/Metadata.py
new file mode 100644
index 000000000..a349805fd
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Lint/Metadata.py
@@ -0,0 +1,148 @@
+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. We
+ count a group tag as a definition if it a) has profile or
+ public set; or b) has any children."""
+ allgroups = [
+ g
+ for g in self.metadata.groups_xml.xdata.xpath("//Groups/Group") +
+ self.metadata.groups_xml.xdata.xpath("//Groups/Group//Group")
+ if g.get("profile") or g.get("public") or g.getchildren()]
+ self.duplicate_entries(allgroups, "group")
+
+ 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..54f6f07d1
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Lint/Pkgmgr.py
@@ -0,0 +1,46 @@
+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 3bf76765b..cf7b51ecc 100644
--- a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
+++ b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py
@@ -167,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
diff --git a/src/lib/Bcfg2/Server/Lint/TemplateHelper.py b/src/lib/Bcfg2/Server/Lint/TemplateHelper.py
new file mode 100644
index 000000000..a24d70cab
--- /dev/null
+++ b/src/lib/Bcfg2/Server/Lint/TemplateHelper.py
@@ -0,0 +1,77 @@
+import sys
+import imp
+from Bcfg2.Server.Lint import ServerPlugin
+from Bcfg2.Server.Plugins.TemplateHelper import HelperModule, MODULE_RE, \
+ safe_module_name
+
+
+class TemplateHelperLint(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 ca9f138ef..2f245561b 100644
--- a/src/lib/Bcfg2/Server/Lint/Validate.py
+++ b/src/lib/Bcfg2/Server/Lint/Validate.py
@@ -6,6 +6,7 @@ import sys
import glob
import fnmatch
import lxml.etree
+import Bcfg2.Options
import Bcfg2.Server.Lint
from Bcfg2.Utils import Executor
@@ -14,6 +15,12 @@ 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)
@@ -58,7 +65,6 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
self.cmd = Executor()
def Run(self):
- schemadir = self.config['schema']
for path, schemaname in self.filesets.items():
try:
@@ -68,7 +74,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:
@@ -165,8 +172,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:
@@ -175,9 +182,8 @@ 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..26de28e7c 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,166 @@ 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__)
+ pass
+ 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):
+ 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.fam.handle_events_in_interval(0.1)
+ 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):
+ 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
diff --git a/src/sbin/bcfg2-lint b/src/sbin/bcfg2-lint
index 4987b8034..e818dc3be 100755
--- a/src/sbin/bcfg2-lint
+++ b/src/sbin/bcfg2-lint
@@ -1,213 +1,8 @@
#!/usr/bin/env python
-
"""This tool examines your Bcfg2 specifications for errors."""
import sys
-import time
-import inspect
-import logging
-import Bcfg2.Logger
-import Bcfg2.Options
-import Bcfg2.Server.Core
-import Bcfg2.Server.Lint
-
-LOGGER = logging.getLogger('bcfg2-lint')
-
-
-def run_serverless_plugins(plugins, errorhandler=None, files=None):
- """ Run serverless plugins """
- LOGGER.debug("Running serverless plugins")
- for plugin_name, plugin in list(plugins.items()):
- run_plugin(plugin, plugin_name, errorhandler=errorhandler, files=files)
-
-
-def run_server_plugins(plugins, errorhandler=None, files=None):
- """ run plugins that require a running server to run """
- core = load_server()
- try:
- LOGGER.debug("Running server plugins")
- for plugin_name, plugin in list(plugins.items()):
- run_plugin(plugin, plugin_name, args=[core],
- errorhandler=errorhandler, files=files)
- finally:
- core.shutdown()
-
-
-def run_plugin(plugin, plugin_name, errorhandler=None, args=None, files=None):
- """ run a single plugin, server-ful or serverless. """
- LOGGER.debug(" Running %s" % plugin_name)
- if args is None:
- args = []
-
- if errorhandler is None:
- errorhandler = get_errorhandler()
-
- setup = Bcfg2.Options.get_option_parser()
- if setup.cfp.has_section(plugin_name):
- arg = setup
- for key, val in setup.cfp.items(plugin_name):
- arg[key] = val
- args.append(arg)
- else:
- args.append(setup)
-
- # python 2.5 doesn't support mixing *magic and keyword arguments
- start = time.time()
- rv = plugin(*args, **dict(files=files, errorhandler=errorhandler)).Run()
- LOGGER.debug(" Ran %s in %0.2f seconds" % (plugin_name,
- time.time() - start))
- return rv
-
-
-def get_errorhandler():
- """ get a Bcfg2.Server.Lint.ErrorHandler object """
- setup = Bcfg2.Options.get_option_parser()
- if setup.cfp.has_section("errors"):
- errors = dict(setup.cfp.items("errors"))
- else:
- errors = None
- return Bcfg2.Server.Lint.ErrorHandler(errors=errors)
-
-
-def load_server():
- """ load server """
- core = Bcfg2.Server.Core.BaseCore()
- core.load_plugins()
- core.fam.handle_events_in_interval(0.1)
- return core
-
-
-def load_plugin(module, obj_name=None):
- """ load a single plugin """
- parts = module.split(".")
- if obj_name is None:
- obj_name = parts[-1]
-
- mod = __import__(module)
- for part in parts[1:]:
- mod = getattr(mod, part)
- return getattr(mod, obj_name)
-
-
-def load_plugins():
- """ get list of plugins to run """
- setup = Bcfg2.Options.get_option_parser()
- if setup['args']:
- plugin_list = setup['args']
- elif "bcfg2-repo-validate" in sys.argv[0]:
- plugin_list = 'RequiredAttrs,Validate'.split(',')
- elif setup['lint_plugins']:
- plugin_list = setup['lint_plugins']
- else:
- plugin_list = Bcfg2.Server.Lint.plugins
-
- allplugins = dict()
- for plugin in plugin_list:
- try:
- allplugins[plugin] = load_plugin("Bcfg2.Server.Lint." + plugin)
- except ImportError:
- try:
- allplugins[plugin] = \
- load_plugin("Bcfg2.Server.Plugins." + plugin,
- obj_name=plugin + "Lint")
- except (ImportError, AttributeError):
- err = sys.exc_info()[1]
- LOGGER.error("Failed to load plugin %s: %s" %
- (plugin + "Lint", err))
- except AttributeError:
- err = sys.exc_info()[1]
- LOGGER.error("Failed to load plugin %s: %s" % (plugin, err))
-
- for plugin in setup['plugins']:
- if plugin in allplugins:
- # already loaded
- continue
-
- try:
- allplugins[plugin] = \
- load_plugin("Bcfg2.Server.Plugins." + plugin,
- obj_name=plugin + "Lint")
- except AttributeError:
- pass
- except ImportError:
- err = sys.exc_info()[1]
- LOGGER.error("Failed to load plugin %s: %s" % (plugin + "Lint",
- err))
-
- serverplugins = dict()
- serverlessplugins = dict()
- for plugin_name, plugin in allplugins.items():
- if [c for c in inspect.getmro(plugin)
- if c == Bcfg2.Server.Lint.ServerPlugin]:
- serverplugins[plugin_name] = plugin
- else:
- serverlessplugins[plugin_name] = plugin
- return (serverlessplugins, serverplugins)
-
-
-def main():
- optinfo = dict(lint_config=Bcfg2.Options.LINT_CONFIG,
- showerrors=Bcfg2.Options.LINT_SHOW_ERRORS,
- stdin=Bcfg2.Options.LINT_FILES_ON_STDIN,
- schema=Bcfg2.Options.SCHEMA_PATH,
- lint_plugins=Bcfg2.Options.LINT_PLUGINS)
- optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS)
- optinfo.update(Bcfg2.Options.SERVER_COMMON_OPTIONS)
- setup = Bcfg2.Options.load_option_parser(optinfo)
- setup.parse(sys.argv[1:])
-
- log_args = dict(to_syslog=setup['syslog'], to_console=logging.WARNING)
- if setup['verbose']:
- log_args['to_console'] = logging.DEBUG
- Bcfg2.Logger.setup_logging('bcfg2-info', **log_args)
-
- setup.cfp.read(setup['lint_config'])
- setup.reparse()
-
- if setup['stdin']:
- files = [s.strip() for s in sys.stdin.readlines()]
- else:
- files = None
-
- (serverlessplugins, serverplugins) = load_plugins()
- errorhandler = get_errorhandler()
-
- if setup['showerrors']:
- for plugin in serverplugins.values() + serverlessplugins.values():
- errorhandler.RegisterErrors(getattr(plugin, 'Errors')())
-
- print("%-35s %-35s" % ("Error name", "Handler"))
- for err, handler in errorhandler.errortypes.items():
- print("%-35s %-35s" % (err, handler.__name__))
- raise SystemExit(0)
-
- run_serverless_plugins(serverlessplugins, errorhandler=errorhandler,
- files=files)
-
- if serverplugins:
- if 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:
- run_server_plugins(serverplugins, errorhandler=errorhandler,
- files=files)
-
- if errorhandler.errors or errorhandler.warnings or setup['verbose']:
- print("%d errors" % errorhandler.errors)
- print("%d warnings" % errorhandler.warnings)
-
- if errorhandler.errors:
- raise SystemExit(2)
- elif errorhandler.warnings:
- raise SystemExit(3)
+from Bcfg2.Server.Lint import CLI
if __name__ == '__main__':
- sys.exit(main())
+ sys.exit(CLI().run())
diff --git a/src/sbin/bcfg2-repo-validate b/src/sbin/bcfg2-repo-validate
deleted file mode 120000
index cea09cda3..000000000
--- a/src/sbin/bcfg2-repo-validate
+++ /dev/null
@@ -1 +0,0 @@
-bcfg2-lint \ No newline at end of file