From 6d4d8df68717780239fad273dd722359db10e64b Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Mon, 24 Sep 2012 13:07:15 -0400 Subject: expanded pylint tests --- src/lib/Bcfg2/Server/BuiltinCore.py | 45 ++-------- src/lib/Bcfg2/Server/CherryPyCore.py | 51 ++++------- src/lib/Bcfg2/Server/Core.py | 137 ++++++++++++++++++++--------- src/lib/Bcfg2/Server/Lint/Comments.py | 38 +++----- src/lib/Bcfg2/Server/Lint/Duplicates.py | 33 ++----- src/lib/Bcfg2/Server/Lint/Genshi.py | 8 +- src/lib/Bcfg2/Server/Lint/GroupNames.py | 21 +++-- src/lib/Bcfg2/Server/Lint/InfoXML.py | 18 ++-- src/lib/Bcfg2/Server/Lint/MergeFiles.py | 29 ++++-- src/lib/Bcfg2/Server/Lint/RequiredAttrs.py | 70 +++++++++------ src/lib/Bcfg2/Server/Lint/Validate.py | 85 +++++++++--------- src/lib/Bcfg2/Server/Lint/__init__.py | 87 +++++++++++------- 12 files changed, 335 insertions(+), 287 deletions(-) (limited to 'src/lib/Bcfg2/Server') diff --git a/src/lib/Bcfg2/Server/BuiltinCore.py b/src/lib/Bcfg2/Server/BuiltinCore.py index 5ceee36b7..ebd426802 100644 --- a/src/lib/Bcfg2/Server/BuiltinCore.py +++ b/src/lib/Bcfg2/Server/BuiltinCore.py @@ -1,23 +1,16 @@ """ the core of the builtin bcfg2 server """ -import os import sys import time import socket import daemon -import logging -from Bcfg2.Server.Core import BaseCore +from Bcfg2.Server.Core import BaseCore, NoExposedMethod from Bcfg2.Compat import xmlrpclib, urlparse from Bcfg2.SSLServer import XMLRPCServer -logger = logging.getLogger() - - -class NoExposedMethod (Exception): - """There is no method exposed with the given name.""" - class Core(BaseCore): + """ The built-in server core """ name = 'bcfg2-server' def __init__(self, setup): @@ -25,21 +18,6 @@ class Core(BaseCore): self.server = None self.context = daemon.DaemonContext() - def _resolve_exposed_method(self, method_name): - """Resolve an exposed method. - - Arguments: - method_name -- name of the method to resolve - - """ - try: - func = getattr(self, method_name) - except AttributeError: - raise NoExposedMethod(method_name) - if not getattr(func, "exposed", False): - raise NoExposedMethod(method_name) - return func - def _dispatch(self, method, args, dispatch_dict): """Custom XML-RPC dispatcher for components. @@ -66,10 +44,10 @@ class Core(BaseCore): except xmlrpclib.Fault: raise except Exception: - e = sys.exc_info()[1] - if getattr(e, "log", True): - self.logger.error(e, exc_info=True) - raise xmlrpclib.Fault(getattr(e, "fault_code", 1), str(e)) + err = sys.exc_info()[1] + if getattr(err, "log", True): + self.logger.error(err, exc_info=True) + raise xmlrpclib.Fault(getattr(err, "fault_code", 1), str(err)) return result def _daemonize(self): @@ -91,10 +69,10 @@ class Core(BaseCore): timeout=1, ca=self.setup['ca'], protocol=self.setup['protocol']) - except: + except: # pylint: disable=W0702 err = sys.exc_info()[1] self.logger.error("Server startup failed: %s" % err) - os._exit(1) + self.context.close() self.server.register_instance(self) def _block(self): @@ -104,10 +82,3 @@ class Core(BaseCore): self.server.server_close() self.context.close() self.shutdown() - - def methodHelp(self, method_name): - try: - func = self._resolve_exposed_method(method_name) - except NoExposedMethod: - return "" - return func.__doc__ diff --git a/src/lib/Bcfg2/Server/CherryPyCore.py b/src/lib/Bcfg2/Server/CherryPyCore.py index 5faed6f6c..684ea4556 100644 --- a/src/lib/Bcfg2/Server/CherryPyCore.py +++ b/src/lib/Bcfg2/Server/CherryPyCore.py @@ -3,14 +3,14 @@ import sys import time import cherrypy -import Bcfg2.Options from Bcfg2.Compat import urlparse, xmlrpclib, b64decode from Bcfg2.Server.Core import BaseCore from cherrypy.lib import xmlrpcutil from cherrypy._cptools import ErrorTool from cherrypy.process.plugins import Daemonizer -def on_error(*args, **kwargs): + +def on_error(*args, **kwargs): # pylint: disable=W0613 """ define our own error handler that handles xmlrpclib.Fault objects and so allows for the possibility of returning proper error codes. this obviates the need to use the builtin CherryPy @@ -18,12 +18,14 @@ def on_error(*args, **kwargs): err = sys.exc_info()[1] if not isinstance(err, xmlrpclib.Fault): err = xmlrpclib.Fault(xmlrpclib.INTERNAL_ERROR, str(err)) - xmlrpcutil._set_response(xmlrpclib.dumps(err)) + xmlrpcutil._set_response(xmlrpclib.dumps(err)) # pylint: disable=W0212 cherrypy.tools.xmlrpc_error = ErrorTool(on_error) class Core(BaseCore): + """ The CherryPy-based server core """ + _cp_config = {'tools.xmlrpc_error.on': True, 'tools.bcfg2_authn.on': True} @@ -37,34 +39,36 @@ class Core(BaseCore): cherrypy.engine.subscribe('stop', self.shutdown) def do_authn(self): + """ perform authentication """ try: header = cherrypy.request.headers['Authorization'] except KeyError: self.critical_error("No authentication data presented") - auth_type, auth_content = header.split() + auth_content = header.split()[1] auth_content = b64decode(auth_content) try: username, password = auth_content.split(":") except ValueError: username = auth_content password = "" - + # FIXME: Get client cert cert = None address = (cherrypy.request.remote.ip, cherrypy.request.remote.name) return self.authenticate(cert, username, password, address) @cherrypy.expose - def default(self, *vpath, **params): - # needed to make enough changes to the stock XMLRPCController - # to support plugin.__rmi__ and prepending client address that - # we just rewrote. it clearly wasn't written with inheritance - # in mind :( + def default(self, *args, **params): # pylint: disable=W0613 + """ needed to make enough changes to the stock + XMLRPCController to support plugin.__rmi__ and prepending + client address that we just rewrote. it clearly wasn't + written with inheritance in mind :( """ rpcparams, rpcmethod = xmlrpcutil.process_body() if rpcmethod == 'ERRORMETHOD': raise Exception("Unknown error processing XML-RPC request body") elif "." not in rpcmethod: - address = (cherrypy.request.remote.ip, cherrypy.request.remote.name) + address = (cherrypy.request.remote.ip, + cherrypy.request.remote.name) rpcparams = (address, ) + rpcparams handler = getattr(self, rpcmethod, None) @@ -81,7 +85,7 @@ class Core(BaseCore): body = handler(*rpcparams, **params) finally: self.stats.add_value(rpcmethod, time.time() - method_start) - + xmlrpcutil.respond(body, 'utf-8', True) return cherrypy.serving.response.body @@ -108,26 +112,3 @@ class Core(BaseCore): def _block(self): cherrypy.engine.block() - - -def parse_opts(argv=None): - if argv is None: - argv = sys.argv[1:] - optinfo = dict() - optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS) - optinfo.update(Bcfg2.Options.SERVER_COMMON_OPTIONS) - optinfo.update(Bcfg2.Options.DAEMON_COMMON_OPTIONS) - setup = Bcfg2.Options.OptionParser(optinfo, argv=argv) - setup.parse(argv) - return setup - -def application(environ, start_response): - """ running behind Apache as a WSGI app is not currently - supported, but I'm keeping this code here because I hope for it to - be supported some day. we'll need to set up an AMQP task queue - and related magic for that to happen, though. """ - cherrypy.config.update({'environment': 'embedded'}) - setup = parse_opts(argv=['-C', environ['config']]) - root = Core(setup) - cherrypy.tree.mount(root) - return cherrypy.tree(environ, start_response) diff --git a/src/lib/Bcfg2/Server/Core.py b/src/lib/Bcfg2/Server/Core.py index 95d8173c6..0e5a88f79 100644 --- a/src/lib/Bcfg2/Server/Core.py +++ b/src/lib/Bcfg2/Server/Core.py @@ -16,7 +16,7 @@ import Bcfg2.Logger import Bcfg2.Server.FileMonitor from Bcfg2.Cache import Cache from Bcfg2.Statistics import Statistics -from Bcfg2.Compat import xmlrpclib, reduce # pylint: disable=W0622 +from Bcfg2.Compat import xmlrpclib, reduce, wraps # pylint: disable=W0622 from Bcfg2.Server.Plugin import PluginInitError, PluginExecutionError try: @@ -29,11 +29,13 @@ os.environ['DJANGO_SETTINGS_MODULE'] = 'Bcfg2.settings' def exposed(func): + """ decorator that sets the 'exposed' attribute of a function to + expose it via XML-RPC """ func.exposed = True return func -class track_statistics(object): +class track_statistics(object): # pylint: disable=C0103 """ decorator that tracks execution time for the given function """ @@ -44,7 +46,9 @@ class track_statistics(object): if self.name is None: self.name = func.__name__ + @wraps(func) def inner(obj, *args, **kwargs): + """ The decorated function """ name = "%s:%s" % (obj.__class__.__name__, self.name) start = time.time() @@ -57,6 +61,7 @@ class track_statistics(object): def sort_xml(node, key=None): + """ sort XML in a deterministic fashion """ for child in node: sort_xml(child, key) @@ -72,12 +77,21 @@ class CoreInitError(Exception): pass +class NoExposedMethod (Exception): + """There is no method exposed with the given name.""" + + +# pylint: disable=W0702 +# in core we frequently want to catch all exceptions, regardless of +# type, so disable the pylint rule that catches that. + + class BaseCore(object): """The Core object is the container for all Bcfg2 Server logic and modules. """ - def __init__(self, setup): + def __init__(self, setup): # pylint: disable=R0912,R0915 self.datastore = setup['repo'] if setup['debug']: @@ -99,18 +113,19 @@ class BaseCore(object): self.logger = logging.getLogger('bcfg2-server') try: - fm = Bcfg2.Server.FileMonitor.available[setup['filemonitor']] + filemonitor = \ + Bcfg2.Server.FileMonitor.available[setup['filemonitor']] except KeyError: self.logger.error("File monitor driver %s not available; " "forcing to default" % setup['filemonitor']) - fm = Bcfg2.Server.FileMonitor.available['default'] + filemonitor = Bcfg2.Server.FileMonitor.available['default'] famargs = dict(ignore=[], debug=False) if 'ignore' in setup: famargs['ignore'] = setup['ignore'] if 'debug' in setup: famargs['debug'] = setup['debug'] try: - self.fam = fm(**famargs) + self.fam = filemonitor(**famargs) except IOError: msg = "Failed to instantiate fam driver %s" % setup['filemonitor'] self.logger.error(msg, exc_info=1) @@ -135,7 +150,8 @@ class BaseCore(object): self._database_available = False # verify our database schema try: - from Bcfg2.Server.SchemaUpdater import update_database, UpdaterError + from Bcfg2.Server.SchemaUpdater import update_database, \ + UpdaterError try: update_database() self._database_available = True @@ -159,21 +175,21 @@ class BaseCore(object): if not plugin in self.plugins: self.init_plugins(plugin) # Remove blacklisted plugins - for p, bl in list(self.plugin_blacklist.items()): - if len(bl) > 0: + for plugin, blacklist in list(self.plugin_blacklist.items()): + if len(blacklist) > 0: self.logger.error("The following plugins conflict with %s;" - "Unloading %s" % (p, bl)) - for plug in bl: + "Unloading %s" % (plugin, blacklist)) + for plug in blacklist: del self.plugins[plug] # This section logs the experimental plugins - expl = [plug for (name, plug) in list(self.plugins.items()) + expl = [plug for plug in list(self.plugins.values()) if plug.experimental] if expl: self.logger.info("Loading experimental plugin(s): %s" % (" ".join([x.name for x in expl]))) self.logger.info("NOTE: Interfaces subject to change") # This section logs the deprecated plugins - depr = [plug for (name, plug) in list(self.plugins.items()) + depr = [plug for plug in list(self.plugins.values()) if plug.deprecated] if depr: self.logger.info("Loading deprecated plugin(s): %s" % @@ -187,7 +203,8 @@ class BaseCore(object): "failed to instantiate Core") raise CoreInitError("No Metadata Plugin") self.statistics = self.plugins_by_type(Bcfg2.Server.Plugin.Statistics) - self.pull_sources = self.plugins_by_type(Bcfg2.Server.Plugin.PullSource) + self.pull_sources = \ + self.plugins_by_type(Bcfg2.Server.Plugin.PullSource) self.generators = self.plugins_by_type(Bcfg2.Server.Plugin.Generator) self.structures = self.plugins_by_type(Bcfg2.Server.Plugin.Structure) self.connectors = self.plugins_by_type(Bcfg2.Server.Plugin.Connector) @@ -239,14 +256,16 @@ class BaseCore(object): (plugin)).Server.Plugins, plugin) except ImportError: try: - mod = __import__(plugin, globals(), locals(), [plugin.split('.')[-1]]) + mod = __import__(plugin, globals(), locals(), + [plugin.split('.')[-1]]) except: self.logger.error("Failed to load plugin %s" % plugin) return try: plug = getattr(mod, plugin.split('.')[-1]) except AttributeError: - self.logger.error("Failed to load plugin %s (AttributeError)" % plugin) + self.logger.error("Failed to load plugin %s (AttributeError)" % + plugin) return # Blacklist conflicting plugins cplugs = [conflict for conflict in plug.conflicts @@ -258,8 +277,8 @@ class BaseCore(object): self.logger.error("Failed to instantiate plugin %s" % plugin, exc_info=1) except: - self.logger.error("Unexpected instantiation failure for plugin %s" % - plugin, exc_info=1) + self.logger.error("Unexpected instantiation failure for plugin %s" + % plugin, exc_info=1) def shutdown(self): """Shutting down the plugins.""" @@ -304,7 +323,8 @@ class BaseCore(object): @track_statistics() def validate_structures(self, metadata, data): """Checks the data structure.""" - for plugin in self.plugins_by_type(Bcfg2.Server.Plugin.StructureValidator): + for plugin in \ + self.plugins_by_type(Bcfg2.Server.Plugin.StructureValidator): try: plugin.validate_structures(metadata, data) except Bcfg2.Server.Plugin.ValidationError: @@ -346,6 +366,8 @@ class BaseCore(object): @track_statistics() def BindStructures(self, structures, metadata, config): + """ Given a list of structures, bind all the entries in them + and add the structures to the config. """ for astruct in structures: try: self.BindStructure(astruct, metadata) @@ -372,8 +394,8 @@ class BaseCore(object): exc = sys.exc_info()[1] if 'failure' not in entry.attrib: entry.set('failure', 'bind error: %s' % format_exc()) - self.logger.error("Unexpected failure in BindStructure: %s %s" % - (entry.tag, entry.get('name')), exc_info=1) + self.logger.error("Unexpected failure in BindStructure: %s %s" + % (entry.tag, entry.get('name')), exc_info=1) def Bind(self, entry, metadata): """Bind an entry using the appropriate generator.""" @@ -393,8 +415,8 @@ class BaseCore(object): self.logger.error("Failed binding entry %s:%s with altsrc %s" % (entry.tag, entry.get('name'), entry.get('altsrc'))) - self.logger.error("Falling back to %s:%s" % (entry.tag, - entry.get('name'))) + self.logger.error("Falling back to %s:%s" % + (entry.tag, entry.get('name'))) glist = [gen for gen in self.generators if entry.get('name') in gen.Entries.get(entry.tag, {})] @@ -557,6 +579,8 @@ class BaseCore(object): self.client_run_hook("end_statistics", meta) def resolve_client(self, address, cleanup_cache=False, metadata=True): + """ given a client address, get the client hostname and + optionally metadata """ try: client = self.metadata.resolve_client(address, cleanup_cache=cleanup_cache) @@ -581,6 +605,7 @@ class BaseCore(object): "Critical failure: %s" % operation) def _get_rmi(self): + """ Get a list of RMI calls exposed by plugins """ rmi = dict() if self.plugins: for pname, pinst in list(self.plugins.items()): @@ -588,9 +613,26 @@ class BaseCore(object): rmi["%s.%s" % (pname, mname)] = getattr(pinst, mname) return rmi + def _resolve_exposed_method(self, method_name): + """Resolve an exposed method. + + Arguments: + method_name -- name of the method to resolve + + """ + try: + func = getattr(self, method_name) + except AttributeError: + raise NoExposedMethod(method_name) + if not getattr(func, "exposed", False): + raise NoExposedMethod(method_name) + return func + # XMLRPC handlers start here + @exposed - def listMethods(self, address): + def listMethods(self, address): # pylint: disable=W0613 + """ list all exposed methods, including plugin RMI """ methods = [name for name, func in inspect.getmembers(self, callable) if getattr(func, "exposed", False)] @@ -598,13 +640,18 @@ class BaseCore(object): return methods @exposed - def methodHelp(self, address, method_name): - raise NotImplementedError + def methodHelp(self, address, method_name): # pylint: disable=W0613 + """ get help on an exposed method """ + try: + func = self._resolve_exposed_method(method_name) + except NoExposedMethod: + return "" + return func.__doc__ @exposed def DeclareVersion(self, address, version): """ declare the client version """ - client, metadata = self.resolve_client(address) + client = self.resolve_client(address, metadata=False)[0] try: self.metadata.set_version(client, version) except (Bcfg2.Server.Plugin.MetadataConsistencyError, @@ -645,26 +692,27 @@ class BaseCore(object): try: xpdata = lxml.etree.XML(probedata.encode('utf-8'), parser=Bcfg2.Server.XMLParser) - except: + except lxml.etree.XMLSyntaxError: err = sys.exc_info()[1] self.critical_error("Failed to parse probe data from client %s: %s" % (client, err)) sources = [] - [sources.append(data.get('source')) for data in xpdata - if data.get('source') not in sources] - for source in sources: - if source not in self.plugins: - self.logger.warning("Failed to locate plugin %s" % source) - continue - dl = [data for data in xpdata if data.get('source') == source] - try: - self.plugins[source].ReceiveData(metadata, dl) - except: - err = sys.exc_info()[1] - self.critical_error("Failed to process probe data from client " - "%s: %s" % - (client, err)) + for data in xpdata: + source = data.get('source') + if source not in sources: + if source not in self.plugins: + self.logger.warning("Failed to locate plugin %s" % source) + continue + datalist = [data for data in xpdata + if data.get('source') == source] + try: + self.plugins[source].ReceiveData(metadata, datalist) + except: + err = sys.exc_info()[1] + self.critical_error("Failed to process probe data from " + "client %s: %s" % + (client, err)) return True @exposed @@ -681,7 +729,7 @@ class BaseCore(object): return True @exposed - def GetConfig(self, address, checksum=False): + def GetConfig(self, address): """Build config for a client.""" client = self.resolve_client(address)[0] try: @@ -701,6 +749,7 @@ class BaseCore(object): return "" def authenticate(self, cert, user, password, address): + """ Authenticate a client connection """ if self.ca: acert = cert else: @@ -712,7 +761,7 @@ class BaseCore(object): @exposed def GetDecisionList(self, address, mode): """Get the data of the decision list.""" - client, metadata = self.resolve_client(address) + metadata = self.resolve_client(address)[1] return self.GetDecisions(metadata, mode) @property diff --git a/src/lib/Bcfg2/Server/Lint/Comments.py b/src/lib/Bcfg2/Server/Lint/Comments.py index 59d18fc57..bb1217f92 100644 --- a/src/lib/Bcfg2/Server/Lint/Comments.py +++ b/src/lib/Bcfg2/Server/Lint/Comments.py @@ -1,12 +1,15 @@ +""" check files for various required comments """ + import os import lxml.etree import Bcfg2.Server.Lint -from Bcfg2.Server import XI, XI_NAMESPACE -from Bcfg2.Server.Plugins.Cfg.CfgPlaintextGenerator import CfgPlaintextGenerator +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.CfgInfoXML import CfgInfoXML + class Comments(Bcfg2.Server.Lint.ServerPlugin): """ check files for various required headers """ def __init__(self, *args, **kwargs): @@ -22,10 +25,9 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin): @classmethod def Errors(cls): - return {"unexpanded-keywords":"warning", - "keywords-not-found":"warning", - "comments-not-found":"warning", - "broken-xinclude-chain":"warning"} + return {"unexpanded-keywords": "warning", + "keywords-not-found": "warning", + "comments-not-found": "warning"} def required_keywords(self, rtype): """ given a file type, fetch the list of required VCS keywords @@ -69,7 +71,8 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin): xdata = lxml.etree.XML(bundle.data) rtype = "bundler" except (lxml.etree.XMLSyntaxError, AttributeError): - xdata = lxml.etree.parse(bundle.template.filepath).getroot() + xdata = \ + lxml.etree.parse(bundle.template.filepath).getroot() rtype = "sgenshi" self.check_xml(bundle.name, xdata, rtype) @@ -153,7 +156,8 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin): if status is None] if unexpanded: self.LintError("unexpanded-keywords", - "%s: Required keywords(s) found but not expanded: %s" % + "%s: Required keywords(s) found but not " + "expanded: %s" % (filename, ", ".join(unexpanded))) missing = [keyword for (keyword, status) in found.items() if status is False] @@ -177,21 +181,3 @@ class Comments(Bcfg2.Server.Lint.ServerPlugin): self.LintError("comments-not-found", "%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('./%sinclude' % XI_NAMESPACE): - if not self.has_all_xincludes(el.get('href')): - self.LintError("broken-xinclude-chain", - "Broken XInclude chain: could not include %s" % path) - return False - - return True - diff --git a/src/lib/Bcfg2/Server/Lint/Duplicates.py b/src/lib/Bcfg2/Server/Lint/Duplicates.py index 60a02ffb9..9b36f054b 100644 --- a/src/lib/Bcfg2/Server/Lint/Duplicates.py +++ b/src/lib/Bcfg2/Server/Lint/Duplicates.py @@ -1,10 +1,11 @@ -import os -import lxml.etree +""" Find duplicate clients, groups, etc. """ + import Bcfg2.Server.Lint -from Bcfg2.Server import XI, XI_NAMESPACE + 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 @@ -25,11 +26,10 @@ class Duplicates(Bcfg2.Server.Lint.ServerPlugin): @classmethod def Errors(cls): - return {"broken-xinclude-chain":"warning", - "duplicate-client":"error", - "duplicate-group":"error", - "duplicate-package":"error", - "multiple-default-groups":"error"} + return {"duplicate-client": "error", + "duplicate-group": "error", + "duplicate-package": "error", + "multiple-default-groups": "error"} def load_xdata(self): """ attempt to load XML data for groups and clients. only @@ -71,20 +71,3 @@ class Duplicates(Bcfg2.Server.Lint.ServerPlugin): self.LintError("multiple-default-groups", "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('./%sinclude' % XI_NAMESPACE): - if not self.has_all_xincludes(el.get('href')): - self.LintError("broken-xinclude-chain", - "Broken XInclude chain: could not include %s" % path) - return False - - return True diff --git a/src/lib/Bcfg2/Server/Lint/Genshi.py b/src/lib/Bcfg2/Server/Lint/Genshi.py index 74142b446..18b4ae28a 100755 --- a/src/lib/Bcfg2/Server/Lint/Genshi.py +++ b/src/lib/Bcfg2/Server/Lint/Genshi.py @@ -1,9 +1,13 @@ +""" Check Genshi templates for syntax errors """ + import sys import genshi.template import Bcfg2.Server.Lint + class Genshi(Bcfg2.Server.Lint.ServerPlugin): """ Check Genshi templates for syntax errors """ + def Run(self): """ run plugin """ loader = genshi.template.TemplateLoader() @@ -14,9 +18,11 @@ class Genshi(Bcfg2.Server.Lint.ServerPlugin): @classmethod def Errors(cls): - return {"genshi-syntax-error":"error"} + return {"genshi-syntax-error": "error"} def check_files(self, entries, loader=None): + """ Check genshi templates in a list of entries for syntax + errors """ if loader is None: loader = genshi.template.TemplateLoader() diff --git a/src/lib/Bcfg2/Server/Lint/GroupNames.py b/src/lib/Bcfg2/Server/Lint/GroupNames.py index 5df98a30e..52e42aa7b 100644 --- a/src/lib/Bcfg2/Server/Lint/GroupNames.py +++ b/src/lib/Bcfg2/Server/Lint/GroupNames.py @@ -1,11 +1,14 @@ +""" ensure that all named groups are valid group names """ + import os import re import Bcfg2.Server.Lint try: from Bcfg2.Server.Plugins.Bundler import BundleTemplateFile - has_genshi = True + HAS_GENSHI = True except ImportError: - has_genshi = False + HAS_GENSHI = False + class GroupNames(Bcfg2.Server.Lint.ServerPlugin): """ ensure that all named groups are valid group names """ @@ -28,6 +31,7 @@ class GroupNames(Bcfg2.Server.Lint.ServerPlugin): return {"invalid-group-name": "error"} def check_rules(self): + """ Check groups used in the Rules plugin for validity """ for rules in self.core.plugins['Rules'].entries.values(): if not self.HandlesFile(rules.name): continue @@ -36,20 +40,23 @@ class GroupNames(Bcfg2.Server.Lint.ServerPlugin): os.path.join(self.config['repo'], rules.name)) def check_bundles(self): - """ check bundles for BoundPath entries with missing attrs """ + """ 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 HAS_GENSHI or not isinstance(bundle, BundleTemplateFile))): self.check_entries(bundle.xdata.xpath("//Group"), bundle.name) def check_metadata(self): + """ 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'], self.metadata.groups_xml.name)) def check_grouppatterns(self): + """ Check groups used in the GroupPatterns plugin for validity """ cfg = self.core.plugins['GroupPatterns'].config if not self.HandlesFile(cfg.name): return @@ -60,7 +67,9 @@ class GroupNames(Bcfg2.Server.Lint.ServerPlugin): (cfg.name, self.RenderXML(grp, keep_text=True))) def check_cfg(self): - for root, dirs, files in os.walk(self.core.plugins['Cfg'].data): + """ Check groups used in group-specific files in the Cfg + plugin for validity """ + for root, _, files in os.walk(self.core.plugins['Cfg'].data): for fname in files: basename = os.path.basename(root) if (re.search(r'^%s\.G\d\d_' % basename, fname) and @@ -71,6 +80,8 @@ class GroupNames(Bcfg2.Server.Lint.ServerPlugin): os.path.join(root, fname)) def check_entries(self, entries, fname): + """ Check a generic list of XML entries for tags with + invalid name attributes """ for grp in entries: if not self.valid.search(grp.get("name")): self.LintError("invalid-group-name", diff --git a/src/lib/Bcfg2/Server/Lint/InfoXML.py b/src/lib/Bcfg2/Server/Lint/InfoXML.py index 5e4e21e18..e34f387ff 100644 --- a/src/lib/Bcfg2/Server/Lint/InfoXML.py +++ b/src/lib/Bcfg2/Server/Lint/InfoXML.py @@ -1,9 +1,12 @@ +""" ensure that all config files have an info.xml file""" + 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): """ ensure that all config files have an info.xml file""" def Run(self): @@ -34,13 +37,14 @@ class InfoXML(Bcfg2.Server.Lint.ServerPlugin): @classmethod def Errors(cls): - return {"no-infoxml":"warning", - "deprecated-info-file":"warning", - "paranoid-false":"warning", - "broken-xinclude-chain":"warning", - "required-infoxml-attrs-missing":"error"} + return {"no-infoxml": "warning", + "deprecated-info-file": "warning", + "paranoid-false": "warning", + "broken-xinclude-chain": "warning", + "required-infoxml-attrs-missing": "error"} def check_infoxml(self, fname, xdata): + """ verify that info.xml contains everything it should """ for info in xdata.getroottree().findall("//Info"): required = [] if "required_attrs" in self.config: @@ -50,7 +54,8 @@ class InfoXML(Bcfg2.Server.Lint.ServerPlugin): if missing: self.LintError("required-infoxml-attrs-missing", "Required attribute(s) %s not found in %s:%s" % - (",".join(missing), fname, self.RenderXML(info))) + (",".join(missing), fname, + self.RenderXML(info))) if ((Bcfg2.Options.MDATA_PARANOID.value and info.get("paranoid") is not None and @@ -61,4 +66,3 @@ class InfoXML(Bcfg2.Server.Lint.ServerPlugin): self.LintError("paranoid-false", "Paranoid must be true in %s:%s" % (fname, self.RenderXML(info))) - diff --git a/src/lib/Bcfg2/Server/Lint/MergeFiles.py b/src/lib/Bcfg2/Server/Lint/MergeFiles.py index 68d010316..44d02c2ff 100644 --- a/src/lib/Bcfg2/Server/Lint/MergeFiles.py +++ b/src/lib/Bcfg2/Server/Lint/MergeFiles.py @@ -1,9 +1,13 @@ +""" find Probes or Cfg files with multiple similar files that might be +merged into one """ + import os import copy from difflib import SequenceMatcher import Bcfg2.Server.Lint from Bcfg2.Server.Plugins.Cfg import CfgGenerator + class MergeFiles(Bcfg2.Server.Lint.ServerPlugin): """ find Probes or Cfg files with multiple similar files that might be merged into one """ @@ -15,11 +19,11 @@ class MergeFiles(Bcfg2.Server.Lint.ServerPlugin): @classmethod def Errors(cls): - return {"merge-cfg":"warning", - "merge-probes":"warning"} - + return {"merge-cfg": "warning", + "merge-probes": "warning"} def check_cfg(self): + """ check Cfg for similar files """ 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)]) @@ -32,6 +36,7 @@ class MergeFiles(Bcfg2.Server.Lint.ServerPlugin): 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): self.LintError("merge-probes", @@ -40,6 +45,9 @@ class MergeFiles(Bcfg2.Server.Lint.ServerPlugin): ", ".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") @@ -61,17 +69,20 @@ class MergeFiles(Bcfg2.Server.Lint.ServerPlugin): return rv def _find_similar(self, ftuple, others, threshold): + """ 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) - sm = SequenceMatcher(None, fdata.data, cdata.data) + seqmatch = SequenceMatcher(None, fdata.data, cdata.data) # perform progressively more expensive comparisons - if (sm.real_quick_ratio() > threshold and - sm.quick_ratio() > threshold and - sm.ratio() > threshold): + 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 - - diff --git a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py index b9d5d79c4..299d6a246 100644 --- a/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py +++ b/src/lib/Bcfg2/Server/Lint/RequiredAttrs.py @@ -1,3 +1,6 @@ +""" verify attributes for configuration entries that cannot be +verified with an XML schema alone""" + import os import re import lxml.etree @@ -7,41 +10,51 @@ import Bcfg2.Client.Tools.VCS from Bcfg2.Server.Plugins.Packages import Apt, Yum try: from Bcfg2.Server.Plugins.Bundler import BundleTemplateFile - has_genshi = True + HAS_GENSHI = True except ImportError: - has_genshi = False + HAS_GENSHI = False + # format verifying functions def is_filename(val): + """ Return True if val is a string describing a valid full path + """ return val.startswith("/") and len(val) > 1 -def is_relative_filename(val): - return len(val) > 1 def is_selinux_type(val): + """ Return True if val is a string describing a valid (although + not necessarily existent) SELinux type """ return re.match(r'^[a-z_]+_t', val) + def is_selinux_user(val): + """ Return True if val is a string describing a valid (although + not necessarily existent) SELinux user """ return re.match(r'^[a-z_]+_u', val) + def is_octal_mode(val): + """ Return True if val is a string describing a valid octal + permissions mode """ return re.match(r'[0-7]{3,4}', val) + def is_username(val): + """ Return True if val is a string giving either a positive + integer uid, or a valid Unix username """ return re.match(r'^([a-z]\w{0,30}|\d+)$', val) + def is_device_mode(val): - try: - # checking upper bound seems like a good way to discover some - # obscure OS with >8-bit device numbers - return int(val) > 0 - except: - return False + """ Return True if val is a string describing a positive integer + """ + return re.match(r'^\d+$', val) class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin): - """ verify attributes for configuration entries (as defined in - doc/server/configurationentries) """ + """ verify attributes for configuration entries that cannot be + verified with an XML schema alone """ def __init__(self, *args, **kwargs): Bcfg2.Server.Lint.ServerPlugin.__init__(self, *args, **kwargs) self.required_attrs = dict( @@ -56,7 +69,7 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin): group=is_username, perms=is_octal_mode, __text__=None), hardlink=dict(name=is_filename, to=is_filename), - symlink=dict(name=is_filename, to=is_relative_filename), + symlink=dict(name=is_filename), ignore=dict(name=is_filename), nonexistent=dict(name=is_filename), permissions=dict(name=is_filename, owner=is_username, @@ -83,7 +96,8 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin): access=dict(scope=lambda v: v in ['user', 'group'], perms=lambda v: re.match('^([0-7]|[rwx\-]{0,3}', v)), - mask=dict(perms=lambda v: re.match('^([0-7]|[rwx\-]{0,3}', v))), + mask=dict(perms=lambda v: re.match('^([0-7]|[rwx\-]{0,3}', + v))), Package={None: dict(name=None)}, SELinux=dict( boolean=dict(name=None, @@ -163,15 +177,17 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin): if 'Bundler' in self.core.plugins: for bundle in self.core.plugins['Bundler'].entries.values(): if (self.HandlesFile(bundle.name) and - (not has_genshi or + (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() + xdata = lxml.etree.parse( + bundle.template.filepath).getroot() - for path in xdata.xpath("//*[substring(name(), 1, 5) = 'Bound']"): + for path in xdata.xpath( + "//*[substring(name(), 1, 5) = 'Bound']" + ): self.check_entry(path, bundle.name) def check_entry(self, entry, filename): @@ -219,14 +235,15 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin): self.RenderXML(entry))) if not attrs.issuperset(required_attrs.keys()): - self.LintError("required-attrs-missing", - "The following required attribute(s) are " - "missing for %s %s in %s: %s\n%s" % - (tag, name, filename, - ", ".join([attr - for attr in - set(required_attrs.keys()).difference(attrs)]), - self.RenderXML(entry))) + self.LintError( + "required-attrs-missing", + "The following required attribute(s) are missing for %s " + "%s in %s: %s\n%s" % + (tag, name, filename, + ", ".join([attr + for attr in + set(required_attrs.keys()).difference(attrs)]), + self.RenderXML(entry))) for attr, fmt in required_attrs.items(): if fmt and attr in attrs and not fmt(entry.attrib[attr]): @@ -235,4 +252,3 @@ class RequiredAttrs(Bcfg2.Server.Lint.ServerPlugin): "malformed\n%s" % (attr, tag, name, filename, self.RenderXML(entry))) - diff --git a/src/lib/Bcfg2/Server/Lint/Validate.py b/src/lib/Bcfg2/Server/Lint/Validate.py index e4c4bddd0..d6fd1df0c 100644 --- a/src/lib/Bcfg2/Server/Lint/Validate.py +++ b/src/lib/Bcfg2/Server/Lint/Validate.py @@ -1,34 +1,38 @@ +""" Ensure that the repo validates """ + import os import sys import glob import fnmatch import lxml.etree from subprocess import Popen, PIPE, STDOUT -from Bcfg2.Server import XI, XI_NAMESPACE +from Bcfg2.Server import XI_NAMESPACE import Bcfg2.Server.Lint + 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":"%s/bundle.xsd", - "%s/Bundler/*.genshi":"%s/bundle.xsd", - "%s/Pkgmgr/*.xml":"%s/pkglist.xsd", - "%s/Base/*.xml":"%s/base.xsd", - "%s/Rules/*.xml":"%s/rules.xsd", - "%s/Defaults/*.xml":"%s/defaults.xsd", - "%s/etc/report-configuration.xml":"%s/report-configuration.xsd", - "%s/Deps/*.xml":"%s/deps.xsd", - "%s/Decisions/*.xml":"%s/decisions.xsd", - "%s/Packages/sources.xml":"%s/packages.xsd", - "%s/GroupPatterns/config.xml":"%s/grouppatterns.xsd", - "%s/NagiosGen/config.xml":"%s/nagiosgen.xsd", - "%s/FileProbes/config.xml":"%s/fileprobes.xsd", - } + self.filesets = \ + {"metadata:groups": "%s/metadata.xsd", + "metadata:clients": "%s/clients.xsd", + "info": "%s/info.xsd", + "%s/Bundler/*.xml": "%s/bundle.xsd", + "%s/Bundler/*.genshi": "%s/bundle.xsd", + "%s/Pkgmgr/*.xml": "%s/pkglist.xsd", + "%s/Base/*.xml": "%s/base.xsd", + "%s/Rules/*.xml": "%s/rules.xsd", + "%s/Defaults/*.xml": "%s/defaults.xsd", + "%s/etc/report-configuration.xml": "%s/report-configuration.xsd", + "%s/Deps/*.xml": "%s/deps.xsd", + "%s/Decisions/*.xml": "%s/decisions.xsd", + "%s/Packages/sources.xml": "%s/packages.xsd", + "%s/GroupPatterns/config.xml": "%s/grouppatterns.xsd", + "%s/NagiosGen/config.xml": "%s/nagiosgen.xsd", + "%s/FileProbes/config.xml": "%s/fileprobes.xsd", + } self.filelists = {} self.get_filelists() @@ -54,13 +58,13 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): @classmethod def Errors(cls): - return {"broken-xinclude-chain":"warning", - "schema-failed-to-parse":"warning", - "properties-schema-not-found":"warning", - "xml-failed-to-parse":"error", - "xml-failed-to-read":"error", - "xml-failed-to-verify":"error", - "input-output-error":"error"} + return {"broken-xinclude-chain": "warning", + "schema-failed-to-parse": "warning", + "properties-schema-not-found": "warning", + "xml-failed-to-parse": "error", + "xml-failed-to-read": "error", + "xml-failed-to-verify": "error", + "input-output-error": "error"} def check_properties(self): """ check Properties files against their schemas """ @@ -124,12 +128,13 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): self.filelists[path] = \ [f for f in self.files if os.path.basename(f) == 'info.xml'] - else: # self.files is None + 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)): + for infodir in ['Cfg', 'SSHbase', 'SSLCA', 'TGenshi', + 'TCheetah']: + for root, _, files in \ + os.walk(os.path.join(self.config['repo'], + infodir)): self.filelists[path].extend([os.path.join(root, f) for f in files if f == 'info.xml']) @@ -146,7 +151,8 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): if (fname not in self.filelists['metadata:groups'] and fname not in self.filelists['metadata:clients']): self.LintError("broken-xinclude-chain", - "Broken XInclude chain: Could not determine file type of %s" % fname) + "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 @@ -163,11 +169,9 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): try: rv.extend(self.follow_xinclude(rv[0])) except lxml.etree.XMLSyntaxError: - e = sys.exc_info()[1] + err = sys.exc_info()[1] self.LintError("xml-failed-to-parse", - "%s fails to parse:\n%s" % (rv[0], e)) - - + "%s fails to parse:\n%s" % (rv[0], err)) return rv def follow_xinclude(self, xfile): @@ -193,21 +197,22 @@ class Validate(Bcfg2.Server.Lint.ServerlessPlugin): elif self.HandlesFile(path): rv.append(path) groupdata = lxml.etree.parse(path) - included.update(el for el in groupdata.findall('./%sinclude' % + included.update(el for el in groupdata.findall('./%sinclude' % XI_NAMESPACE)) included.discard(filename) return rv def _load_schema(self, filename): + """ load an XML schema document, returning the Schema object """ try: return lxml.etree.XMLSchema(lxml.etree.parse(filename)) except IOError: - e = sys.exc_info()[1] - self.LintError("input-output-error", str(e)) + err = sys.exc_info()[1] + self.LintError("input-output-error", str(err)) except lxml.etree.XMLSchemaParseError: - e = sys.exc_info()[1] + err = sys.exc_info()[1] self.LintError("schema-failed-to-parse", "Failed to process schema %s: %s" % - (filename, e)) + (filename, err)) return None diff --git a/src/lib/Bcfg2/Server/Lint/__init__.py b/src/lib/Bcfg2/Server/Lint/__init__.py index f4a9b74c2..eea205b75 100644 --- a/src/lib/Bcfg2/Server/Lint/__init__.py +++ b/src/lib/Bcfg2/Server/Lint/__init__.py @@ -1,51 +1,47 @@ -__all__ = ['Bundler', - 'Comments', - 'Duplicates', - 'InfoXML', - 'MergeFiles', - 'Pkgmgr', - 'RequiredAttrs', - 'Validate', - 'Genshi'] +""" Base classes for Lint plugins and error handling """ -import logging import os import sys +import logging from copy import copy import textwrap import lxml.etree -import Bcfg2.Logger import fcntl import termios import struct +from Bcfg2.Server import XI_NAMESPACE -def _ioctl_GWINSZ(fd): + +def _ioctl_GWINSZ(fd): # pylint: disable=C0103 + """ get a tuple of (height, width) giving the size of the window + from the given file descriptor """ try: - cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) - except: + return struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234')) + except: # pylint: disable=W0702 return None - return cr + def get_termsize(): """ get a tuple of (width, height) giving the size of the terminal """ if not sys.stdout.isatty(): return None - cr = _ioctl_GWINSZ(0) or _ioctl_GWINSZ(1) or _ioctl_GWINSZ(2) - if not cr: + dims = _ioctl_GWINSZ(0) or _ioctl_GWINSZ(1) or _ioctl_GWINSZ(2) + if not dims: try: fd = os.open(os.ctermid(), os.O_RDONLY) - cr = _ioctl_GWINSZ(fd) + dims = _ioctl_GWINSZ(fd) os.close(fd) - except: + except: # pylint: disable=W0702 pass - if not cr: + if not dims: try: - cr = (os.environ['LINES'], os.environ['COLUMNS']) + dims = (os.environ['LINES'], os.environ['COLUMNS']) except KeyError: return None - return int(cr[1]), int(cr[0]) + return int(dims[1]), int(dims[0]) -class Plugin (object): + +class Plugin(object): """ base class for ServerlessPlugin and ServerPlugin """ def __init__(self, config, errorhandler=None, files=None): @@ -78,6 +74,7 @@ class Plugin (object): fname)) in self.files) def LintError(self, err, msg): + """ record an error in the lint process """ self.errorhandler.dispatch(err, msg) def RenderXML(self, element, keep_text=False): @@ -88,16 +85,20 @@ class Plugin (object): el = copy(element) if el.text and not keep_text: el.text = '...' - [el.remove(c) for c in el.iterchildren()] - xml = lxml.etree.tostring(el, - xml_declaration=False).decode("UTF-8").strip() + for child in el.iterchildren(): + el.remove(child) + xml = lxml.etree.tostring( + el, + xml_declaration=False).decode("UTF-8").strip() else: - xml = lxml.etree.tostring(element, - xml_declaration=False).decode("UTF-8").strip() + xml = lxml.etree.tostring( + element, + xml_declaration=False).decode("UTF-8").strip() return " line %s: %s" % (element.sourceline, xml) class ErrorHandler (object): + """ a class to handle errors for bcfg2-lint plugins """ def __init__(self, config=None): self.errors = 0 self.warnings = 0 @@ -124,6 +125,8 @@ class ErrorHandler (object): self._handlers[err] = self.debug def RegisterErrors(self, errors): + """ Register a dict of errors (name: default level) that a + plugin may raise """ for err, action in errors.items(): if err not in self._handlers: if "warn" in action: @@ -132,8 +135,9 @@ class ErrorHandler (object): self._handlers[err] = self.error else: self._handlers[err] = self.debug - + def dispatch(self, err, msg): + """ Dispatch an error to the correct handler """ if err in self._handlers: self._handlers[err](msg) self.logger.debug(" (%s)" % err) @@ -157,6 +161,8 @@ class ErrorHandler (object): self._log(msg, self.logger.debug) def _log(self, msg, logfunc, prefix=""): + """ Generic log function that logs a message with the given + function after wrapping it for the terminal width """ # a message may itself consist of multiple lines. wrap() will # elide them all into a single paragraph, which we don't want. # so we split the message into its paragraphs and wrap each @@ -186,8 +192,27 @@ class ServerlessPlugin (Plugin): class ServerPlugin (Plugin): """ base class for plugins that check things that require the running Bcfg2 server """ - def __init__(self, lintCore, config, **kwargs): + def __init__(self, core, config, **kwargs): Plugin.__init__(self, config, **kwargs) - self.core = lintCore + self.core = core self.logger = self.core.logger self.metadata = self.core.metadata + self.errorhandler.RegisterErrors({"broken-xinclude-chain": "warning"}) + + 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('./%sinclude' % XI_NAMESPACE): + if not self.has_all_xincludes(el.get('href')): + self.LintError("broken-xinclude-chain", + "Broken XInclude chain: could not " + "include %s" % path) + return False + + return True -- cgit v1.2.3-1-g7c22