summaryrefslogtreecommitdiffstats
path: root/src/lib
diff options
context:
space:
mode:
authorChris St. Pierre <chris.a.st.pierre@gmail.com>2011-04-20 09:41:07 -0400
committerChris St. Pierre <chris.a.st.pierre@gmail.com>2011-04-20 09:41:07 -0400
commitb5810882e8c6b1e6b76a8239f70a129d415ecee6 (patch)
tree8c2df3610bebd92f52b70b7f37a7197c9ec2a3e9 /src/lib
parent20974e1311168b75e621cad14894fe7b217b61a2 (diff)
downloadbcfg2-b5810882e8c6b1e6b76a8239f70a129d415ecee6.tar.gz
bcfg2-b5810882e8c6b1e6b76a8239f70a129d415ecee6.tar.bz2
bcfg2-b5810882e8c6b1e6b76a8239f70a129d415ecee6.zip
Rewrote bcfg2-repo-validate as bcfg2-lint, which uses a plugin
interface to be lots more flexible and extensible. Added several more tests. If bcfg2-lint is run as bcfg2-repo-validate, it roughly emulates the functionality of that program. TODO: Need to figure out correct way to symlink bcfg2-repo-validate to bcfg2-lint on install.
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/Logger.py12
-rw-r--r--src/lib/Server/Lint/Bundles.py56
-rw-r--r--src/lib/Server/Lint/Comments.py183
-rw-r--r--src/lib/Server/Lint/Duplicates.py79
-rw-r--r--src/lib/Server/Lint/InfoXML.py38
-rw-r--r--src/lib/Server/Lint/Pkgmgr.py33
-rw-r--r--src/lib/Server/Lint/RequiredAttrs.py69
-rw-r--r--src/lib/Server/Lint/Validate.py185
-rw-r--r--src/lib/Server/Lint/__init__.py90
9 files changed, 743 insertions, 2 deletions
diff --git a/src/lib/Logger.py b/src/lib/Logger.py
index a9c4372b7..ae73a6d41 100644
--- a/src/lib/Logger.py
+++ b/src/lib/Logger.py
@@ -158,17 +158,23 @@ class FragmentingSysLogHandler(logging.handlers.SysLogHandler):
pass
-def setup_logging(procname, to_console=True, to_syslog=True, syslog_facility='daemon', level=0, to_file=None):
+def setup_logging(procname, to_console=True, to_syslog=True,
+ syslog_facility='daemon', level=0, to_file=None):
"""Setup logging for Bcfg2 software."""
if hasattr(logging, 'already_setup'):
return
+
# add the handler to the root logger
if to_console:
console = logging.StreamHandler(sys.stdout)
- console.setLevel(logging.DEBUG)
+ if to_console is True:
+ console.setLevel(logging.DEBUG)
+ else:
+ console.setLevel(to_console)
# tell the handler to use this format
console.setFormatter(TermiosFormatter())
logging.root.addHandler(console)
+
if to_syslog:
try:
try:
@@ -186,11 +192,13 @@ def setup_logging(procname, to_console=True, to_syslog=True, syslog_facility='da
logging.root.error("failed to activate syslogging")
except:
print("Failed to activate syslogging")
+
if not to_file == None:
filelog = logging.FileHandler(to_file)
filelog.setLevel(logging.DEBUG)
filelog.setFormatter(logging.Formatter('%(asctime)s %(name)s[%(process)d]: %(message)s'))
logging.root.addHandler(filelog)
+
logging.root.setLevel(level)
logging.already_setup = True
diff --git a/src/lib/Server/Lint/Bundles.py b/src/lib/Server/Lint/Bundles.py
new file mode 100644
index 000000000..a1ce631c9
--- /dev/null
+++ b/src/lib/Server/Lint/Bundles.py
@@ -0,0 +1,56 @@
+import lxml.etree
+import Bcfg2.Server.Lint
+
+class Bundles(Bcfg2.Server.Lint.ServerPlugin):
+ """ Perform various bundle checks """
+
+ @Bcfg2.Server.Lint.returnErrors
+ def Run(self):
+ """ run plugin """
+ self.missing_bundles()
+ self.bundle_names()
+ self.sgenshi_groups()
+
+ def missing_bundles(self):
+ """ find bundles listed in Metadata but not implemented in Bundler """
+ groupdata = self.metadata.groups_xml.xdata
+ ref_bundles = set([b.get("name")
+ for b in groupdata.findall("//Bundle")])
+
+ allbundles = self.core.plugins['Bundler'].entries.keys()
+ for bundle in ref_bundles:
+ xmlbundle = "%s.xml" % bundle
+ genshibundle = "%s.genshi" % bundle
+ if xmlbundle not in allbundles and genshibundle not in allbundles:
+ self.LintError("Bundle %s referenced, but does not exist" %
+ bundle)
+
+ def bundle_names(self):
+ """ verify bundle name attribute matches filename """
+ for bundle in self.core.plugins['Bundler'].entries.values():
+ if self.HandlesFile(bundle.name):
+ try:
+ xdata = lxml.etree.XML(bundle.data)
+ except AttributeError:
+ # genshi template
+ xdata = lxml.etree.parse(bundle.template.filepath).getroot()
+
+ fname = bundle.name.split('Bundler/')[1].split('.')[0]
+ bname = xdata.get('name')
+ if fname != bname:
+ self.LintWarning("Inconsistent bundle name: filename is %s, bundle name is %s" %
+ (fname, bname))
+
+ def sgenshi_groups(self):
+ """ ensure that Genshi Bundles do not include <Group> tags,
+ which are not supported """
+ for bundle in self.core.plugins['Bundler'].entries.values():
+ if self.HandlesFile(bundle.name):
+ if (type(bundle) is
+ Bcfg2.Server.Plugins.SGenshi.SGenshiTemplateFile):
+ xdata = lxml.etree.parse(bundle.name)
+ groups = [self.RenderXML(g)
+ for g in xdata.getroottree().findall("//Group")]
+ if groups:
+ self.LintError("<Group> tag is not allowed in SGenshi Bundle:\n%s" %
+ "\n".join(groups))
diff --git a/src/lib/Server/Lint/Comments.py b/src/lib/Server/Lint/Comments.py
new file mode 100644
index 000000000..0b50df373
--- /dev/null
+++ b/src/lib/Server/Lint/Comments.py
@@ -0,0 +1,183 @@
+import os.path
+import lxml.etree
+import Bcfg2.Server.Lint
+
+class Comments(Bcfg2.Server.Lint.ServerPlugin):
+ """ check files for various required headers """
+ def __init__(self, *args, **kwargs):
+ Bcfg2.Server.Lint.ServerPlugin.__init__(self, *args, **kwargs)
+ self.config_cache = {}
+
+ @Bcfg2.Server.Lint.returnErrors
+ def Run(self):
+ self.check_bundles()
+ self.check_properties()
+ self.check_metadata()
+ self.check_cfg()
+ self.check_infoxml()
+ self.check_probes()
+
+ def required_keywords(self, rtype):
+ """ given a file type, fetch the list of required VCS keywords
+ from the bcfg2-lint config """
+ return self.required_items(rtype, "keyword", default=["Id"])
+
+ def required_comments(self, rtype):
+ """ given a file type, fetch the list of required comments
+ from the bcfg2-lint config """
+ return self.required_items(rtype, "comment")
+
+ def required_items(self, rtype, itype, default=None):
+ """ given a file type and item type (comment or keyword),
+ fetch the list of required items from the bcfg2-lint config """
+ if itype not in self.config_cache:
+ self.config_cache[itype] = {}
+
+ 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(","))
+ elif default is not None:
+ rv.extend(default)
+
+ 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 = []
+ self.config_cache[itype][rtype] = rv
+ return self.config_cache[itype][rtype]
+
+ def check_bundles(self):
+ """ check bundle files for required headers """
+ for bundle in self.core.plugins['Bundler'].entries.values():
+ xdata = None
+ rtype = ""
+ try:
+ xdata = lxml.etree.XML(bundle.data)
+ rtype = "bundler"
+ except AttributeError:
+ xdata = lxml.etree.parse(bundle.template.filepath).getroot()
+ rtype = "sgenshi"
+
+ self.check_xml(bundle.name, xdata, rtype)
+
+ def check_properties(self):
+ """ check properties files for required headers """
+ if 'Properties' in self.core.plugins:
+ props = self.core.plugins['Properties']
+ for propfile, pdata in props.store.entries.items():
+ if os.path.splitext(propfile)[1] == ".xml":
+ self.check_xml(pdata.name, pdata.data, 'properties')
+
+ def check_metadata(self):
+ """ check metadata files for required headers """
+ metadata = self.core.plugins['Metadata']
+ if self.has_all_xincludes("groups.xml"):
+ self.check_xml(os.path.join(metadata.data, "groups.xml"),
+ metadata.groups_xml.data,
+ "metadata")
+ if self.has_all_xincludes("clients.xml"):
+ self.check_xml(os.path.join(metadata.data, "clients.xml"),
+ metadata.clients_xml.data,
+ "metadata")
+
+ def check_cfg(self):
+ """ check Cfg files for required headers """
+ for entryset in self.core.plugins['Cfg'].entries.values():
+ for entry in entryset.entries.values():
+ if entry.name.endswith(".genshi"):
+ rtype = "tgenshi"
+ else:
+ rtype = "cfg"
+ self.check_plaintext(entry.name, entry.data, rtype)
+
+ def check_infoxml(self):
+ """ check info.xml files for required headers """
+ for entryset in self.core.plugins['Cfg'].entries.items():
+ if hasattr(entryset, "infoxml") and entryset.infoxml is not None:
+ self.check_xml(entryset.infoxml.name,
+ entryset.infoxml.pnode.data,
+ "infoxml")
+
+ def check_probes(self):
+ """ check probes for required headers """
+ if 'Probes' in self.core.plugins:
+ for probe in self.core.plugins['Probes'].probes.entries.values():
+ self.check_plaintext(probe.name, probe.data, "probes")
+
+ def check_xml(self, filename, xdata, rtype):
+ """ check generic XML files for required headers """
+ self.check_lines(filename,
+ [str(el)
+ for el in xdata.getiterator(lxml.etree.Comment)],
+ rtype)
+
+ def check_plaintext(self, filename, data, rtype):
+ """ check generic plaintex files for required headers """
+ self.check_lines(filename, data.splitlines(), rtype)
+
+ def check_lines(self, filename, lines, rtype):
+ """ generic header check for a set of lines """
+ if self.HandlesFile(filename):
+ # found is trivalent:
+ # False == not found
+ # None == found but not expanded
+ # True == found and expanded
+ found = dict((k, False) for k in self.required_keywords(rtype))
+
+ for line in lines:
+ # we check for both '$<keyword>:' and '$<keyword>$' to see
+ # if the keyword just hasn't been expanded
+ for (keyword, status) in found.items():
+ if not status:
+ if '$%s:' % keyword in line:
+ found[keyword] = True
+ elif '$%s$' % keyword in line:
+ found[keyword] = None
+
+ unexpanded = [keyword for (keyword, status) in found.items()
+ if status is None]
+ if unexpanded:
+ self.LintError("%s: Required keywords(s) found but not expanded: %s" %
+ (filename, ", ".join(unexpanded)))
+ missing = [keyword for (keyword, status) in found.items()
+ if status is False]
+ if missing:
+ self.LintError("%s: Required keywords(s) not found: %s" %
+ (filename, ", ".join(missing)))
+
+ # next, check for required comments. found is just
+ # boolean
+ found = dict((k, False) for k in self.required_comments(rtype))
+
+ for line in lines:
+ for (comment, status) in found.items():
+ if not status:
+ found[comment] = comment in line
+
+ missing = [comment for (comment, status) in found.items()
+ if status is False]
+ if missing:
+ self.LintError("%s: Required comments(s) not found: %s" %
+ (filename, ", ".join(missing)))
+
+ def has_all_xincludes(self, mfile):
+ """ return true if self.files includes all XIncludes listed in
+ the specified metadata type, false otherwise"""
+ if self.files is None:
+ return True
+ else:
+ path = os.path.join(self.metadata.data, mfile)
+ if path in self.files:
+ xdata = lxml.etree.parse(path)
+ for el in xdata.findall('./{http://www.w3.org/2001/XInclude}include'):
+ if not self.has_all_xincludes(el.get('href')):
+ self.LintWarning("Broken XInclude chain: could not include %s" % path)
+ return False
+
+ return True
+
diff --git a/src/lib/Server/Lint/Duplicates.py b/src/lib/Server/Lint/Duplicates.py
new file mode 100644
index 000000000..c8b542025
--- /dev/null
+++ b/src/lib/Server/Lint/Duplicates.py
@@ -0,0 +1,79 @@
+import os.path
+import lxml.etree
+import Bcfg2.Server.Lint
+
+class Duplicates(Bcfg2.Server.Lint.ServerPlugin):
+ """ Find duplicate clients, groups, etc. """
+ def __init__(self, *args, **kwargs):
+ Bcfg2.Server.Lint.ServerPlugin.__init__(self, *args, **kwargs)
+ self.groups_xdata = None
+ self.clients_xdata = None
+ self.load_xdata()
+
+ @Bcfg2.Server.Lint.returnErrors
+ def Run(self):
+ """ run plugin """
+ # only run this plugin if we were not given a list of files.
+ # not only is it marginally silly to run this plugin with a
+ # partial list of files, it turns out to be really freaking
+ # hard to get only a fragment of group or client metadata
+ if self.groups_xdata is not None:
+ self.duplicate_groups()
+ self.duplicate_defaults()
+ if self.clients_xdata is not None:
+ self.duplicate_clients()
+
+ def load_xdata(self):
+ """ attempt to load XML data for groups and clients. only
+ actually load data if all documents reference in XIncludes can
+ be found in self.files"""
+ if self.has_all_xincludes("groups.xml"):
+ self.groups_xdata = self.metadata.clients_xml.xdata
+ if self.has_all_xincludes("clients.xml"):
+ self.clients_xdata = self.metadata.clients_xml.xdata
+
+ def duplicate_groups(self):
+ """ find duplicate groups """
+ self.duplicate_entries(self.clients_xdata.xpath('//Groups/Group'),
+ 'group')
+
+ def duplicate_clients(self):
+ """ find duplicate clients """
+ self.duplicate_entries(self.clients_xdata.xpath('//Clients/Client'),
+ 'client')
+
+ def duplicate_entries(self, data, etype):
+ """ generic duplicate entry finder """
+ seen = {}
+ for el in data:
+ if el.get('name') not in seen:
+ seen[el.get('name')] = el
+ else:
+ self.LintError("Duplicate %s '%s':\n%s\n%s" %
+ (etype, el.get('name'),
+ self.RenderXML(seen[el.get('name')]),
+ self.RenderXML(el)))
+
+ def duplicate_defaults(self):
+ """ check for multiple default group definitions """
+ default_groups = [g for g in self.groups_xdata.findall('.//Group')
+ if g.get('default') == 'true']
+ if len(default_groups) > 1:
+ self.LintError("Multiple default groups defined: %s" %
+ ",".join(default_groups))
+
+ def has_all_xincludes(self, mfile):
+ """ return true if self.files includes all XIncludes listed in
+ the specified metadata type, false otherwise"""
+ if self.files is None:
+ return True
+ else:
+ path = os.path.join(self.metadata.data, mfile)
+ if path in self.files:
+ xdata = lxml.etree.parse(path)
+ for el in xdata.findall('./{http://www.w3.org/2001/XInclude}include'):
+ if not self.has_all_xincludes(el.get('href')):
+ self.LintWarning("Broken XInclude chain: could not include %s" % path)
+ return False
+
+ return True
diff --git a/src/lib/Server/Lint/InfoXML.py b/src/lib/Server/Lint/InfoXML.py
new file mode 100644
index 000000000..097c2d6f9
--- /dev/null
+++ b/src/lib/Server/Lint/InfoXML.py
@@ -0,0 +1,38 @@
+import os.path
+import Bcfg2.Options
+import Bcfg2.Server.Lint
+
+class InfoXML(Bcfg2.Server.Lint.ServerPlugin):
+ """ ensure that all config files have an info.xml file"""
+
+ @Bcfg2.Server.Lint.returnErrors
+ def Run(self):
+ for filename, entryset in self.core.plugins['Cfg'].entries.items():
+ infoxml_fname = os.path.join(entryset.path, "info.xml")
+ if self.HandlesFile(infoxml_fname):
+ if (hasattr(entryset, "infoxml") and
+ entryset.infoxml is not None):
+ xdata = entryset.infoxml.pnode.data
+ for info in xdata.getroottree().findall("//Info"):
+ required = ["owner", "group", "perms"]
+ if "required" in self.config:
+ required = self.config["required"].split(",")
+
+ missing = [attr for attr in required
+ if info.get(attr) is None]
+ if missing:
+ self.LintError("Required attribute(s) %s not found in %s:%s" %
+ (",".join(missing), infoxml_fname,
+ self.RenderXML(info)))
+
+ if ("require_paranoid" in self.config and
+ self.config["require_paranoid"].lower() == "true" and
+ not Bcfg2.Options.MDATA_PARANOID.value and
+ info.get("paranoid").lower() != "true"):
+ self.LintError("Paranoid must be true in %s:%s" %
+ (infoxml_fname,
+ self.RenderXML(info)))
+ elif ("require" in self.config and
+ self.config["require"].lower != "false"):
+ self.LintError("No info.xml found for %s" % filename)
+
diff --git a/src/lib/Server/Lint/Pkgmgr.py b/src/lib/Server/Lint/Pkgmgr.py
new file mode 100644
index 000000000..28ca698bd
--- /dev/null
+++ b/src/lib/Server/Lint/Pkgmgr.py
@@ -0,0 +1,33 @@
+import Bcfg2.Server.Lint
+
+class Pkgmgr(Bcfg2.Server.Lint.ServerPlugin):
+ """ find duplicate Pkgmgr entries with the same priority """
+
+ @Bcfg2.Server.Lint.returnErrors
+ def Run(self):
+ pset = set()
+ for plist in self.core.plugins['Pkgmgr'].entries.values():
+ if self.HandlesFile(plist.name):
+ xdata = plist.data
+ # get priority, type, group
+ priority = xdata.getroot().get('priority')
+ ptype = xdata.getroot().get('type')
+ for pkg in xdata.findall("//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.LintWarning("Duplicate Package %s, priority:%s, type:%s" %
+ (pkg.get('name'), priority, ptype))
+ else:
+ pset.add(ptuple)
diff --git a/src/lib/Server/Lint/RequiredAttrs.py b/src/lib/Server/Lint/RequiredAttrs.py
new file mode 100644
index 000000000..7215fe163
--- /dev/null
+++ b/src/lib/Server/Lint/RequiredAttrs.py
@@ -0,0 +1,69 @@
+import os.path
+import lxml.etree
+import Bcfg2.Server.Lint
+
+class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin):
+ """ verify attributes for configuration entries (as defined in
+ doc/server/configurationentries) """
+
+ def __init__(self, *args, **kwargs):
+ Bcfg2.Server.Lint.ServerPlugin.__init__(self, *args, **kwargs)
+ self.required_attrs = {
+ 'device': ['name', 'owner', 'group', 'dev_type'],
+ 'directory': ['name', 'owner', 'group', 'perms'],
+ 'file': ['name', 'owner', 'group', 'perms'],
+ 'hardlink': ['name', 'to'],
+ 'symlink': ['name', 'to'],
+ 'ignore': ['name'],
+ 'nonexistent': ['name'],
+ 'permissions': ['name', 'owner', 'group', 'perms']}
+
+ @Bcfg2.Server.Lint.returnErrors
+ def Run(self):
+ self.check_rules()
+ self.check_bundles()
+
+ def check_rules(self):
+ """ check Rules for Path entries with missing attrs """
+ if 'Rules' in self.core.plugins:
+ 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))
+
+ def check_bundles(self):
+ """ check bundles for BoundPath entries with missing attrs """
+ for bundle in self.core.plugins['Bundler'].entries.values():
+ try:
+ xdata = lxml.etree.XML(bundle.data)
+ except AttributeError:
+ xdata = lxml.etree.parse(bundle.template.filepath).getroot()
+
+ for path in xdata.xpath("//BoundPath"):
+ self.check_entry(path, bundle.name)
+
+ def check_entry(self, entry, filename):
+ """ generic entry check """
+ if self.HandlesFile(filename):
+ pathname = entry.get('name')
+ pathtype = entry.get('type')
+ pathset = set(entry.attrib.keys())
+ try:
+ required_attrs = set(self.required_attrs[pathtype] + ['type'])
+ except KeyError:
+ self.LintError("Unknown path type %s: %s" %
+ (pathtype, self.RenderXML(entry)))
+
+ if 'dev_type' in required_attrs:
+ dev_type = entry.get('dev_type')
+ if dev_type in ['block', 'char']:
+ # check if major/minor are specified
+ required_attrs |= set(['major', 'minor'])
+ if not pathset.issuperset(required_attrs):
+ self.LintError("The required attributes %s are missing for %s %sin %s:\n%s" %
+ (",".join([attr
+ for attr in
+ required_attrs.difference(pathset)]),
+ entry.tag, pathname, filename,
+ self.RenderXML(entry)))
diff --git a/src/lib/Server/Lint/Validate.py b/src/lib/Server/Lint/Validate.py
new file mode 100644
index 000000000..bb5af93f4
--- /dev/null
+++ b/src/lib/Server/Lint/Validate.py
@@ -0,0 +1,185 @@
+import glob
+import lxml.etree
+import os
+import fnmatch
+import Bcfg2.Options
+import Bcfg2.Server.Lint
+from subprocess import Popen, PIPE, STDOUT
+
+class Validate(Bcfg2.Server.Lint.ServerlessPlugin):
+ """ Ensure that the repo validates """
+
+ def __init__(self, *args, **kwargs):
+ Bcfg2.Server.Lint.ServerlessPlugin.__init__(self, *args, **kwargs)
+ self.filesets = {"metadata:groups":"%s/metadata.xsd",
+ "metadata:clients":"%s/clients.xsd",
+ "info":"%s/info.xsd",
+ "%s/Bundler/*.{xml,genshi}":"%s/bundle.xsd",
+ "%s/Pkgmgr/*.xml":"%s/pkglist.xsd",
+ "%s/Base/*.xml":"%s/base.xsd",
+ "%s/Rules/*.xml":"%s/rules.xsd",
+ "%s/etc/report-configuration.xml":"%s/report-configuration.xsd",
+ "%s/Svcmgr/*.xml":"%s/services.xsd",
+ "%s/Deps/*.xml":"%s/deps.xsd",
+ "%s/Decisions/*.xml":"%s/decisions.xsd",
+ "%s/Packages/config.xml":"%s/packages.xsd",
+ "%s/GroupPatterns/config.xml":"%s/grouppatterns.xsd"}
+
+ self.filelists = {}
+ self.get_filelists()
+
+ @Bcfg2.Server.Lint.returnErrors
+ def Run(self):
+ self.schemadir = self.config['schema']
+
+ for schemaname, path in self.filesets.items():
+ try:
+ filelist = self.filelists[path]
+ except KeyError:
+ filelist = []
+
+ if filelist:
+ # avoid loading schemas for empty file lists
+ try:
+ schema = lxml.etree.XMLSchema(lxml.etree.parse(schemaname %
+ schemadir))
+ except:
+ self.LintWarning("Failed to process schema %s",
+ schemaname % schemadir)
+ continue
+ for filename in filelist:
+ self.validate(filename, schemaname % schemadir,
+ schema=schema)
+
+ self.check_properties()
+
+ def check_properties(self):
+ """ check Properties files against their schemas """
+ alert = self.logger.debug
+ if "properties_schema" in self.config:
+ if self.config['properties_schema'].lower().startswith('warn'):
+ alert = self.LintWarning
+ elif self.config['properties_schema'].lower().startswith('require'):
+ alert = self.LintError
+
+ for filename in self.filelists['props']:
+ schemafile = "%s.xsd" % os.path.splitext(filename)[0]
+ if os.path.exists(schemafile):
+ self.validate(filename, schemafile)
+ else:
+ alert("No schema found for %s" % filename)
+
+ def validate(self, filename, schemafile, schema=None):
+ """validate a file against the given lxml.etree.Schema.
+ return True on success, False on failure """
+ if schema is None:
+ # if no schema object was provided, instantiate one
+ try:
+ schema = lxml.etree.XMLSchema(lxml.etree.parse(schemafile))
+ except:
+ self.LintWarning("Failed to process schema %s" % schemafile)
+ return False
+
+ try:
+ datafile = lxml.etree.parse(filename)
+ except SyntaxError:
+ lint = Popen(["xmllint", filename], stdout=PIPE, stderr=STDOUT)
+ self.LintError("%s fails to parse:\n%s" % (filename,
+ lint.communicate()[0]))
+ lint.wait()
+ return False
+ except IOError:
+ self.LintError("Failed to open file %s" % filename)
+ return False
+
+ if not schema.validate(datafile):
+ cmd = ["xmllint"]
+ 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]
+ if lint.wait():
+ self.LintError("%s fails to verify:\n%s" % (filename, output))
+ return False
+ return True
+
+ def get_filelists(self):
+ """ get lists of different kinds of files to validate """
+ if self.files is not None:
+ listfiles = lambda p: fnmatch.filter(self.files, p % "*")
+ else:
+ listfiles = lambda p: glob.glob(p % self.config['repo'])
+
+ for path in self.filesets.keys():
+ if path.startswith("metadata:"):
+ mtype = path.split(":")[1]
+ self.filelists[path] = self.get_metadata_list(mtype)
+ elif path == "info":
+ if self.files is not None:
+ self.filelists[path] = \
+ [f for f in self.files
+ if os.path.basename(f) == 'info.xml']
+ else: # self.files is None
+ self.filelists[path] = []
+ for infodir in ['Cfg', 'TGenshi', 'TCheetah']:
+ for root, dirs, files in os.walk('%s/%s' %
+ (self.config['repo'],
+ infodir)):
+ self.filelists[path].extend([os.path.join(root, f)
+ for f in files
+ if f == 'info.xml'])
+ else:
+ self.filelists[path] = listfiles(path)
+
+ self.filelists['props'] = listfiles("%s/Properties/*.xml")
+ all_metadata = listfiles("%s/Metadata/*.xml")
+
+ # if there are other files in Metadata that aren't xincluded
+ # from clients.xml or groups.xml, we can't verify them. warn
+ # about those.
+ for fname in all_metadata:
+ if (fname not in self.filelists['metadata:groups'] and
+ fname not in self.filelists['metadata:clients']):
+ self.LintWarning("Broken XInclude chain: Could not determine file type of %s" % fname)
+
+ def get_metadata_list(self, mtype):
+ """ get all metadata files for the specified type (clients or
+ group) """
+ if self.files is not None:
+ rv = fnmatch.filter(self.files, "*/Metadata/%s.xml" % mtype)
+ else:
+ rv = glob.glob("%s/Metadata/%s.xml" % (self.config['repo'], mtype))
+
+ # attempt to follow XIncludes. if the top-level files aren't
+ # listed in self.files, though, there's really nothing we can
+ # do to guess what a file in Metadata is
+ if rv:
+ rv.extend(self.follow_xinclude(rv[0]))
+
+ return rv
+
+ def follow_xinclude(self, xfile):
+ """ follow xincludes in the given file """
+ xdata = lxml.etree.parse(xfile)
+ included = set([ent.get('href') for ent in
+ xdata.findall('./{http://www.w3.org/2001/XInclude}include')])
+ rv = []
+
+ while included:
+ try:
+ filename = included.pop()
+ except KeyError:
+ continue
+
+ path = os.path.join(os.path.dirname(xfile), filename)
+ if self.HandlesFile(path):
+ rv.append(path)
+ groupdata = lxml.etree.parse(path)
+ [included.add(el.get('href'))
+ for el in
+ groupdata.findall('./{http://www.w3.org/2001/XInclude}include')]
+ included.discard(filename)
+
+ return rv
+
diff --git a/src/lib/Server/Lint/__init__.py b/src/lib/Server/Lint/__init__.py
new file mode 100644
index 000000000..4e6d03fb5
--- /dev/null
+++ b/src/lib/Server/Lint/__init__.py
@@ -0,0 +1,90 @@
+__revision__ = '$Revision$'
+
+__all__ = ['Bundles',
+ 'Comments',
+ 'Duplicates',
+ 'InfoXML',
+ 'Pkgmgr',
+ 'RequiredAttrs',
+ 'Validate']
+
+import logging
+import os.path
+from copy import copy
+import lxml.etree
+import Bcfg2.Logger
+
+def returnErrors(fn):
+ """ Decorator for Run method that returns error counts """
+ def run(self, *args, **kwargs):
+ fn(self, *args, **kwargs)
+ return (self.error_count, self.warning_count)
+
+ return run
+
+class Plugin (object):
+ """ base class for ServerlessPlugin and ServerPlugin """
+ def __init__(self, config, files=None):
+ self.files = files
+ self.error_count = 0
+ self.warning_count = 0
+ self.config = config
+ Bcfg2.Logger.setup_logging('bcfg2-info', to_syslog=False)
+ self.logger = logging.getLogger('bcfg2-lint')
+
+ def Run(self):
+ """ run the plugin. must be overloaded by child classes """
+ pass
+
+ def HandlesFile(self, fname):
+ """ returns true if the given file should be handled by the
+ plugin according to the files list, 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.abspath(fname) in self.files or
+ os.path.abspath(os.path.join(self.config['repo'],
+ fname)) in self.files)
+
+ def LintError(self, msg):
+ """ log an error condition """
+ self.error_count += 1
+ lines = msg.splitlines()
+ self.logger.error("ERROR: %s" % lines.pop())
+ [self.logger.error(" %s" % l) for l in lines]
+
+ def LintWarning(self, msg):
+ """ log a warning condition """
+ self.warning_count += 1
+ lines = msg.splitlines()
+ self.logger.warning("WARNING: %s" % lines.pop())
+ [self.logger.warning(" %s" % l) for l in lines]
+
+ def RenderXML(self, element):
+ """render an XML element for error output -- line number
+ prefixed, no children"""
+ xml = None
+ if len(element) or element.text:
+ el = copy(element)
+ if el.text:
+ el.text = '...'
+ [el.remove(c) for c in el.iterchildren()]
+ xml = lxml.etree.tostring(el).strip()
+ else:
+ xml = lxml.etree.tostring(element).strip()
+ return " line %s: %s" % (element.sourceline, xml)
+
+class ServerlessPlugin (Plugin):
+ """ base class for plugins that are run before the server starts
+ up (i.e., plugins that check things that may prevent the server
+ from starting up) """
+ pass
+
+class ServerPlugin (Plugin):
+ """ base class for plugins that check things that require the
+ running Bcfg2 server """
+ def __init__(self, lintCore, config, files=None):
+ Plugin.__init__(self, config, files=files)
+ self.core = lintCore
+ self.logger = self.core.logger
+ self.metadata = self.core.metadata