diff options
author | Chris St. Pierre <chris.a.st.pierre@gmail.com> | 2012-09-25 11:49:15 -0400 |
---|---|---|
committer | Chris St. Pierre <chris.a.st.pierre@gmail.com> | 2012-09-25 11:58:48 -0400 |
commit | eac71fc1109f2edc6b71e62a6cff38d762bebe63 (patch) | |
tree | 203cf372e31b92dfc0cf7ea57c451c44e5e1e54b /src | |
parent | 3f16355e18cdceb37828a00a8181d9cc60815cd0 (diff) | |
download | bcfg2-eac71fc1109f2edc6b71e62a6cff38d762bebe63.tar.gz bcfg2-eac71fc1109f2edc6b71e62a6cff38d762bebe63.tar.bz2 bcfg2-eac71fc1109f2edc6b71e62a6cff38d762bebe63.zip |
expanded pylint coverage
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/Bcfg2/Cache.py | 4 | ||||
-rw-r--r-- | src/lib/Bcfg2/Client/Frame.py | 171 | ||||
-rw-r--r-- | src/lib/Bcfg2/Client/XML.py | 17 | ||||
-rw-r--r-- | src/lib/Bcfg2/Compat.py | 48 | ||||
-rwxr-xr-x | src/lib/Bcfg2/Encryption.py | 3 | ||||
-rw-r--r-- | src/lib/Bcfg2/Logger.py | 40 | ||||
-rw-r--r-- | src/lib/Bcfg2/Options.py | 102 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/FileMonitor/Fam.py | 31 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/FileMonitor/Gamin.py | 4 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/FileMonitor/Inotify.py | 39 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/FileMonitor/Pseudo.py | 11 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/FileMonitor/__init__.py | 45 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Lint/__init__.py | 26 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Plugins/Metadata.py | 320 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/Plugins/Probes.py | 6 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/__init__.py | 4 | ||||
-rw-r--r-- | src/lib/Bcfg2/Server/models.py | 29 | ||||
-rw-r--r-- | src/lib/Bcfg2/Statistics.py | 11 | ||||
-rw-r--r-- | src/lib/Bcfg2/settings.py | 27 | ||||
-rw-r--r-- | src/lib/Bcfg2/version.py | 61 | ||||
-rwxr-xr-x | src/sbin/bcfg2-crypt | 100 | ||||
-rwxr-xr-x | src/sbin/bcfg2-lint | 134 | ||||
-rwxr-xr-x | src/sbin/bcfg2-test | 4 |
23 files changed, 733 insertions, 504 deletions
diff --git a/src/lib/Bcfg2/Cache.py b/src/lib/Bcfg2/Cache.py index 9a828e2c9..842098eda 100644 --- a/src/lib/Bcfg2/Cache.py +++ b/src/lib/Bcfg2/Cache.py @@ -2,11 +2,13 @@ doesn't provide many features, but more (time-based expiration, etc.) can be added as necessary. """ + class Cache(dict): """ an implementation of a simple memory-backed cache """ + def expire(self, key=None): + """ expire all items, or a specific item, from the cache """ if key is None: self.clear() elif key in self: del self[key] - diff --git a/src/lib/Bcfg2/Client/Frame.py b/src/lib/Bcfg2/Client/Frame.py index 2fb81d6ba..ef61940eb 100644 --- a/src/lib/Bcfg2/Client/Frame.py +++ b/src/lib/Bcfg2/Client/Frame.py @@ -1,13 +1,12 @@ -""" -Frame is the Client Framework that verifies and -installs entries, and generates statistics. -""" +""" Frame is the Client Framework that verifies and installs entries, +and generates statistics. """ -import logging import sys import time +import fnmatch +import logging import Bcfg2.Client.Tools -from Bcfg2.Compat import input +from Bcfg2.Compat import input, any, all # pylint: disable=W0622 def cmpent(ent1, ent2): @@ -19,35 +18,36 @@ def cmpent(ent1, ent2): def matches_entry(entryspec, entry): - # both are (tag, name) + """ Determine if the Decisions-style entry specification matches + the entry. Both are tuples of (tag, name). The entryspec can + handle the wildcard * in either position. """ if entryspec == entry: return True - else: - for i in [0, 1]: - if entryspec[i] == entry[i]: - continue - elif entryspec[i] == '*': - continue - elif '*' in entryspec[i]: - starpt = entryspec[i].index('*') - if entry[i].startswith(entryspec[i][:starpt]): - continue - return False - return True + return all(fnmatch.fnmatch(entry[i], entryspec[i]) for i in [0, 1]) def matches_white_list(entry, whitelist): - return True in [matches_entry(we, (entry.tag, entry.get('name'))) - for we in whitelist] + """ Return True if (<entry tag>, <entry name>) is in the given + whitelist. """ + return any(matches_entry(we, (entry.tag, entry.get('name'))) + for we in whitelist) def passes_black_list(entry, blacklist): - return True not in [matches_entry(be, (entry.tag, entry.get('name'))) - for be in blacklist] + """ Return True if (<entry tag>, <entry name>) is not in the given + blacklist. """ + return not any(matches_entry(be, (entry.tag, entry.get('name'))) + for be in blacklist) + + +# pylint: disable=W0702 +# in frame we frequently want to catch all exceptions, regardless of +# type, so disable the pylint rule that catches that. class Frame(object): """Frame is the container for all Tool objects and state information.""" + def __init__(self, config, setup, times, drivers, dryrun): self.config = config self.times = times @@ -92,8 +92,9 @@ class Frame(object): for tool in self.tools[:]: for conflict in getattr(tool, 'conflicts', []): - [self.tools.remove(item) for item in self.tools \ - if item.name == conflict] + for item in self.tools: + if item.name == conflict: + self.tools.remove(item) self.logger.info("Loaded tool drivers:") self.logger.info([tool.name for tool in self.tools]) @@ -104,7 +105,8 @@ class Frame(object): if entry not in self.handled] if self.unhandled: - self.logger.error("The following entries are not handled by any tool:") + self.logger.error("The following entries are not handled by any " + "tool:") for entry in self.unhandled: self.logger.error("%s:%s:%s" % (entry.tag, entry.get('type'), entry.get('name'))) @@ -118,10 +120,12 @@ class Frame(object): if pkgs: self.logger.debug("The following packages are specified in bcfg2:") self.logger.debug([pkg[0] for pkg in pkgs if pkg[1] == None]) - self.logger.debug("The following packages are prereqs added by Packages:") + self.logger.debug("The following packages are prereqs added by " + "Packages:") self.logger.debug([pkg[0] for pkg in pkgs if pkg[1] == 'Packages']) def find_dups(self, config): + """ Find duplicate entries and warn about them """ entries = dict() for struct in config: for entry in struct: @@ -134,7 +138,8 @@ class Frame(object): entries[pkey] = 1 multi = [e for e, c in entries.items() if c > 1] if multi: - self.logger.debug("The following entries are included multiple times:") + self.logger.debug("The following entries are included multiple " + "times:") for entry in multi: self.logger.debug(entry) @@ -179,7 +184,8 @@ class Frame(object): non-whitelisted/blacklisted 'important' entries from being installed prior to determining the decision mode on the client. """ - # Need to process decision stuff early so that dryrun mode works with it + # Need to process decision stuff early so that dryrun mode + # works with it self.whitelist = [entry for entry in self.states \ if not self.states[entry]] if not self.setup['file']: @@ -188,17 +194,23 @@ class Frame(object): w_to_rem = [e for e in self.whitelist \ if not matches_white_list(e, dwl)] if w_to_rem: - self.logger.info("In whitelist mode: suppressing installation of:") - self.logger.info(["%s:%s" % (e.tag, e.get('name')) for e in w_to_rem]) + self.logger.info("In whitelist mode: " + "suppressing installation of:") + self.logger.info(["%s:%s" % (e.tag, e.get('name')) + for e in w_to_rem]) self.whitelist = [x for x in self.whitelist \ if x not in w_to_rem] elif self.setup['decision'] == 'blacklist': - b_to_rem = [e for e in self.whitelist \ - if not passes_black_list(e, self.setup['decision_list'])] + b_to_rem = \ + [e for e in self.whitelist + if not passes_black_list(e, self.setup['decision_list'])] if b_to_rem: - self.logger.info("In blacklist mode: suppressing installation of:") - self.logger.info(["%s:%s" % (e.tag, e.get('name')) for e in b_to_rem]) - self.whitelist = [x for x in self.whitelist if x not in b_to_rem] + self.logger.info("In blacklist mode: " + "suppressing installation of:") + self.logger.info(["%s:%s" % (e.tag, e.get('name')) + for e in b_to_rem]) + self.whitelist = [x for x in self.whitelist + if x not in b_to_rem] # take care of important entries first if not self.dryrun: @@ -216,22 +228,22 @@ class Frame(object): (parent.tag == "Independent" and (self.setup['bundle'] or self.setup['skipindep']))): continue - tl = [t for t in self.tools - if t.handlesEntry(cfile) and t.canVerify(cfile)] - if tl: - if self.setup['interactive'] and not \ - self.promptFilter("Install %s: %s? (y/N):", [cfile]): + tools = [t for t in self.tools + if t.handlesEntry(cfile) and t.canVerify(cfile)] + if tools: + if (self.setup['interactive'] and not + self.promptFilter("Install %s: %s? (y/N):", [cfile])): self.whitelist.remove(cfile) continue try: - self.states[cfile] = tl[0].InstallPath(cfile) + self.states[cfile] = tools[0].InstallPath(cfile) if self.states[cfile]: - tl[0].modified.append(cfile) + tools[0].modified.append(cfile) except: self.logger.error("Unexpected tool failure", exc_info=1) cfile.set('qtext', '') - if tl[0].VerifyPath(cfile, []): + if tools[0].VerifyPath(cfile, []): self.whitelist.remove(cfile) def Inventory(self): @@ -249,9 +261,10 @@ class Frame(object): try: tool.Inventory(self.states) except: - self.logger.error("%s.Inventory() call failed:" % tool.name, exc_info=1) + self.logger.error("%s.Inventory() call failed:" % tool.name, + exc_info=1) - def Decide(self): + def Decide(self): # pylint: disable=R0912 """Set self.whitelist based on user interaction.""" prompt = "Install %s: %s? (y/N): " rprompt = "Remove %s: %s? (y/N): " @@ -270,12 +283,14 @@ class Frame(object): if self.dryrun: if self.whitelist: - self.logger.info("In dryrun mode: suppressing entry installation for:") + self.logger.info("In dryrun mode: " + "suppressing entry installation for:") self.logger.info(["%s:%s" % (entry.tag, entry.get('name')) for entry in self.whitelist]) self.whitelist = [] if self.removal: - self.logger.info("In dryrun mode: suppressing entry removal for:") + self.logger.info("In dryrun mode: " + "suppressing entry removal for:") self.logger.info(["%s:%s" % (entry.tag, entry.get('name')) for entry in self.removal]) self.removal = [] @@ -301,7 +316,7 @@ class Frame(object): if bundle not in all_bundle_names: self.logger.info("Warning: Bundle %s not found" % bundle) - bundles = filter(lambda b: \ + bundles = filter(lambda b: b.get('name') not in self.setup['skipbundle'], bundles) if self.setup['skipindep']: @@ -314,7 +329,8 @@ class Frame(object): for bundle in bundles[:]: if bundle.tag != 'Bundle': continue - bmodified = len([item for item in bundle if item in self.whitelist]) + bmodified = len([item for item in bundle + if item in self.whitelist]) actions = [a for a in bundle.findall('./Action') if (a.get('timing') != 'post' and (bmodified or a.get('when') == 'always'))] @@ -335,7 +351,8 @@ class Frame(object): (bundle.get('name'))) self.logger.info(["%s:%s" % (e.tag, e.get('name')) for e in b_to_remv]) - [self.whitelist.remove(ent) for ent in b_to_remv] + for ent in b_to_remv: + self.whitelist.remove(ent) if self.setup['interactive']: self.whitelist = self.promptFilter(prompt, self.whitelist) @@ -354,7 +371,8 @@ class Frame(object): try: tool.Install(handled, self.states) except: - self.logger.error("%s.Install() call failed:" % tool.name, exc_info=1) + self.logger.error("%s.Install() call failed:" % tool.name, + exc_info=1) def Install(self): """Install all entries.""" @@ -373,9 +391,12 @@ class Frame(object): try: tool.Inventory(self.states, [bundle]) except: - self.logger.error("%s.Inventory() call failed:" % tool.name, exc_info=1) + self.logger.error("%s.Inventory() call failed:" % + tool.name, + exc_info=1) clobbered = [entry for bundle in mbundles for entry in bundle \ - if not self.states[entry] and entry not in self.blacklist] + if (not self.states[entry] and + entry not in self.blacklist)] if clobbered: self.logger.debug("Found clobbered entries:") self.logger.debug(["%s:%s" % (entry.tag, entry.get('name')) \ @@ -395,18 +416,20 @@ class Frame(object): else: tool.BundleNotUpdated(bundle, self.states) except: - self.logger.error("%s.BundleNotUpdated() call failed:" % \ - (tool.name), exc_info=1) + self.logger.error("%s.BundleNotUpdated() call failed:" % + tool.name, exc_info=1) def Remove(self): """Remove extra entries.""" for tool in self.tools: - extras = [entry for entry in self.removal if tool.handlesEntry(entry)] + extras = [entry for entry in self.removal + if tool.handlesEntry(entry)] if extras: try: tool.Remove(extras) except: - self.logger.error("%s.Remove() failed" % tool.name, exc_info=1) + self.logger.error("%s.Remove() failed" % tool.name, + exc_info=1) def CondDisplayState(self, phase): """Conditionally print tracing information.""" @@ -420,8 +443,8 @@ class Frame(object): if not self.states[entry]: etype = entry.get('type') if etype: - self.logger.info( "%s:%s:%s" % (entry.tag, etype, - entry.get('name'))) + self.logger.info("%s:%s:%s" % (entry.tag, etype, + entry.get('name'))) else: self.logger.info(" %s:%s" % (entry.tag, entry.get('name'))) @@ -432,8 +455,8 @@ class Frame(object): for entry in self.extra: etype = entry.get('type') if etype: - self.logger.info( "%s:%s:%s" % (entry.tag, etype, - entry.get('name'))) + self.logger.info("%s:%s:%s" % (entry.tag, etype, + entry.get('name'))) else: self.logger.info(" %s:%s" % (entry.tag, entry.get('name'))) @@ -467,24 +490,26 @@ class Frame(object): def GenerateStats(self): """Generate XML summary of execution statistics.""" feedback = Bcfg2.Client.XML.Element("upload-statistics") - stats = Bcfg2.Client.XML.SubElement(feedback, - 'Statistics', - total=str(len(self.states)), - version='2.0', - revision=self.config.get('revision', '-1')) + stats = Bcfg2.Client.XML.SubElement( + feedback, + 'Statistics', + total=str(len(self.states)), + version='2.0', + revision=self.config.get('revision', '-1')) good_entries = [key for key, val in list(self.states.items()) if val] good = len(good_entries) stats.set('good', str(good)) - if len([key for key, val in list(self.states.items()) if not val]) == 0: - stats.set('state', 'clean') - else: + if any(not val for val in list(self.states.values())): stats.set('state', 'dirty') + else: + stats.set('state', 'clean') # List bad elements of the configuration - for (data, ename) in [(self.modified, 'Modified'), (self.extra, "Extra"), \ + for (data, ename) in [(self.modified, 'Modified'), + (self.extra, "Extra"), (good_entries, "Good"), - ([entry for entry in self.states if not \ - self.states[entry]], "Bad")]: + ([entry for entry in self.states + if not self.states[entry]], "Bad")]: container = Bcfg2.Client.XML.SubElement(stats, ename) for item in data: item.set('qtext', '') diff --git a/src/lib/Bcfg2/Client/XML.py b/src/lib/Bcfg2/Client/XML.py index d6bbd3b72..720416724 100644 --- a/src/lib/Bcfg2/Client/XML.py +++ b/src/lib/Bcfg2/Client/XML.py @@ -2,7 +2,7 @@ # library will use lxml, then builtin xml.etree, then ElementTree -# pylint: disable=F0401,E0611 +# pylint: disable=F0401,E0611,W0611,W0613,C0103 try: from lxml.etree import Element, SubElement, XML, tostring @@ -16,8 +16,11 @@ except ImportError: Element = xml.etree.ElementTree.Element SubElement = xml.etree.ElementTree.SubElement XML = xml.etree.ElementTree.XML - def tostring(e, encoding=None, xml_declaration=None): - return xml.etree.ElementTree.tostring(e, encoding=encoding) + + def tostring(el, encoding=None, xml_declaration=None): + """ tostring implementation compatible with lxml """ + return xml.etree.ElementTree.tostring(el, encoding=encoding) + driver = 'etree-py' except ImportError: try: @@ -28,10 +31,12 @@ except ImportError: Element = elementtree.ElementTree.Element SubElement = elementtree.ElementTree.SubElement XML = elementtree.ElementTree.XML - def tostring(e, encoding=None, xml_declaration=None): - return elementtree.ElementTree.tostring(e) + + def tostring(el, encoding=None, xml_declaration=None): + """ tostring implementation compatible with lxml """ + return elementtree.ElementTree.tostring(el) except ImportError: - print("Failed to load lxml, xml.etree and elementtree.ElementTree") + print("Failed to load lxml, xml.etree or elementtree.ElementTree") print("Cannot continue") raise SystemExit(1) diff --git a/src/lib/Bcfg2/Compat.py b/src/lib/Bcfg2/Compat.py index 9aeda6d36..f466b8e03 100644 --- a/src/lib/Bcfg2/Compat.py +++ b/src/lib/Bcfg2/Compat.py @@ -3,7 +3,7 @@ Python 2.4 and such-like """ import sys -# pylint: disable=F0401,E0611 +# pylint: disable=F0401,E0611,W0611,W0622,C0103 try: from email.Utils import formatdate @@ -75,8 +75,9 @@ if sys.hexversion >= 0x03000000: else: unicode = unicode -# print to file compatibility + def u_str(string, encoding=None): + """ print to file compatibility """ if sys.hexversion >= 0x03000000: if encoding is not None: return string.encode(encoding) @@ -90,7 +91,7 @@ def u_str(string, encoding=None): try: unicode = unicode -except: +except NameError: unicode = str # base64 compat @@ -103,7 +104,7 @@ else: try: input = raw_input -except: +except NameError: input = input try: @@ -117,24 +118,25 @@ except ImportError: from UserDict import DictMixin as MutableMapping -# in py3k __cmp__ is no longer magical, so we define a mixin that can -# be used to define the rich comparison operators from __cmp__ class CmpMixin(object): + """ in py3k __cmp__ is no longer magical, so this mixin can be + used to define the rich comparison operators from __cmp__ """ + def __lt__(self, other): return self.__cmp__(other) < 0 - + def __gt__(self, other): return self.__cmp__(other) > 0 - + def __eq__(self, other): return self.__cmp__(other) == 0 - + def __ne__(self, other): return not self.__eq__(other) - + def __ge__(self, other): return self.__gt__(other) or self.__eq__(other) - + def __le__(self, other): return self.__lt__(other) or self.__eq__(other) @@ -145,11 +147,16 @@ except ImportError: from pkgutil import iter_modules # iter_modules was added in python 2.5; use it to get an exact # re-implementation of walk_packages if possible + def walk_packages(path=None, prefix='', onerror=None): - def seen(p, m={}): - if p in m: + """ Implementation of walk_packages for python 2.5 """ + def seen(path, seenpaths={}): # pylint: disable=W0102 + """ detect if a path has been 'seen' (i.e., considered + for inclusion in the generator). tracks what has been + seen through the magic of python default arguments """ + if path in seenpaths: return True - m[p] = True + seenpaths[path] = True for importer, name, ispkg in iter_modules(path, prefix): yield importer, name, ispkg @@ -179,7 +186,7 @@ except ImportError: def walk_packages(path=None, prefix='', onerror=None): """ imperfect, incomplete implementation of walk_packages() for python 2.4. Differences: - + * requires a full path, not a path relative to something in sys.path. anywhere we care about that shouldn't be an issue @@ -187,14 +194,8 @@ except ImportError: * the first element of each tuple is None instead of an importer object """ - def seen(p, m={}): - if p in m: - return True - m[p] = True - if path is None: path = sys.path - rv = [] for mpath in path: for fname in os.listdir(mpath): fpath = os.path.join(mpath, fname) @@ -227,12 +228,14 @@ try: any = any except NameError: def all(iterable): + """ implementation of builtin all() for python 2.4 """ for element in iterable: if not element: return False return True def any(iterable): + """ implementation of builtin any() for python 2.4 """ for element in iterable: if element: return True @@ -247,5 +250,6 @@ except ImportError: try: from functools import wraps except ImportError: - def wraps(wrapped): + def wraps(wrapped): # pylint: disable=W0613 + """ implementation of functools.wraps() for python 2.4 """ return lambda f: f diff --git a/src/lib/Bcfg2/Encryption.py b/src/lib/Bcfg2/Encryption.py index 5eb7ffe8e..eb2841bb5 100755 --- a/src/lib/Bcfg2/Encryption.py +++ b/src/lib/Bcfg2/Encryption.py @@ -33,6 +33,8 @@ Rand.rand_seed(os.urandom(1024)) def _cipher_filter(cipher, instr): + """ M2Crypto reads and writes file-like objects, so this uses + StringIO to pass data through it """ inbuf = StringIO(instr) outbuf = StringIO() while 1: @@ -161,6 +163,7 @@ def get_algorithm(setup): return setup.cfp.get("encryption", "algorithm", default=ALGORITHM).lower().replace("-", "_") + def get_passphrases(setup): """ Get all candidate encryption passphrases from the config file. diff --git a/src/lib/Bcfg2/Logger.py b/src/lib/Bcfg2/Logger.py index 1f4ba7dd0..06379ce7b 100644 --- a/src/lib/Bcfg2/Logger.py +++ b/src/lib/Bcfg2/Logger.py @@ -29,7 +29,7 @@ class TermiosFormatter(logging.Formatter): "\000" * 8))[1] if self.width == 0: self.width = 80 - except: + except: # pylint: disable=W0702 self.width = 80 else: # output to a pipe @@ -44,22 +44,24 @@ class TermiosFormatter(logging.Formatter): if len(line) <= line_len: returns.append(line) else: - inner_lines = int(math.floor(float(len(line)) / line_len)) + 1 - for i in range(inner_lines): - returns.append("%s" % (line[i * line_len:(i + 1) * line_len])) + inner_lines = \ + int(math.floor(float(len(line)) / line_len)) + 1 + for msgline in range(inner_lines): + returns.append( + line[msgline * line_len:(msgline + 1) * line_len]) elif isinstance(record.msg, list): if not record.msg: return '' record.msg.sort() msgwidth = self.width - columnWidth = max([len(item) for item in record.msg]) - columns = int(math.floor(float(msgwidth) / (columnWidth + 2))) + col_width = max([len(item) for item in record.msg]) + columns = int(math.floor(float(msgwidth) / (col_width + 2))) lines = int(math.ceil(float(len(record.msg)) / columns)) - for lineNumber in range(lines): - indices = [idx for idx in [(colNum * lines) + lineNumber + for lineno in range(lines): + indices = [idx for idx in [(colNum * lines) + lineno for colNum in range(columns)] if idx < len(record.msg)] - retformat = (len(indices) * (" %%-%ds " % columnWidth)) + retformat = (len(indices) * (" %%-%ds " % col_width)) returns.append(retformat % tuple([record.msg[idx] for idx in indices])) else: @@ -99,13 +101,13 @@ class FragmentingSysLogHandler(logging.handlers.SysLogHandler): else: msgs = [record] for newrec in msgs: - msg = '<%d>%s\000' % (self.encodePriority(self.facility, - newrec.levelname.lower()), - self.format(newrec)) + msg = '<%d>%s\000' % \ + (self.encodePriority(self.facility, newrec.levelname.lower()), + self.format(newrec)) try: self.socket.send(msg.encode('ascii')) except socket.error: - for i in range(10): + for i in range(10): # pylint: disable=W0612 try: if isinstance(self.address, tuple): self.socket = socket.socket(socket.AF_INET, @@ -124,12 +126,13 @@ class FragmentingSysLogHandler(logging.handlers.SysLogHandler): logging.WARNING), self.format(reconn))) self.socket.send(msg) - except: + except: # pylint: disable=W0702 # If we still fail then drop it. Running # bcfg2-server as non-root can trigger permission # denied exceptions. pass + def add_console_handler(level=logging.DEBUG): """Add a logging handler that logs at a level to sys.stdout.""" console = logging.StreamHandler(sys.stdout) @@ -138,6 +141,7 @@ def add_console_handler(level=logging.DEBUG): console.setFormatter(TermiosFormatter()) logging.root.addHandler(console) + def add_syslog_handler(procname, syslog_facility, level=logging.DEBUG): """Add a logging handler that logs as procname to syslog_facility.""" try: @@ -150,20 +154,24 @@ def add_syslog_handler(procname, syslog_facility, level=logging.DEBUG): ('localhost', 514), syslog_facility) syslog.setLevel(level) - syslog.setFormatter(logging.Formatter('%(name)s[%(process)d]: %(message)s')) + syslog.setFormatter( + logging.Formatter('%(name)s[%(process)d]: %(message)s')) logging.root.addHandler(syslog) except socket.error: logging.root.error("failed to activate syslogging") except: print("Failed to activate syslogging") + def add_file_handler(to_file, level=logging.DEBUG): """Add a logging handler that logs to to_file.""" filelog = logging.FileHandler(to_file) filelog.setLevel(level) - filelog.setFormatter(logging.Formatter('%(asctime)s %(name)s[%(process)d]: %(message)s')) + filelog.setFormatter( + logging.Formatter('%(asctime)s %(name)s[%(process)d]: %(message)s')) logging.root.addHandler(filelog) + def setup_logging(procname, to_console=True, to_syslog=True, syslog_facility='daemon', level=0, to_file=None): """Setup logging for Bcfg2 software.""" diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py index ff7c3ce70..a5436dbd0 100644 --- a/src/lib/Bcfg2/Options.py +++ b/src/lib/Bcfg2/Options.py @@ -8,12 +8,12 @@ import re import shlex import sys import Bcfg2.Client.Tools -# Compatibility imports from Bcfg2.Compat import ConfigParser from Bcfg2.version import __version__ class OptionFailure(Exception): + """ raised when malformed Option objects are instantiated """ pass DEFAULT_CONFIG_LOCATION = '/etc/bcfg2.conf' @@ -21,6 +21,9 @@ DEFAULT_INSTALL_PREFIX = '/usr' class DefaultConfigParser(ConfigParser.ConfigParser): + """ A config parser that can be used to query options with default + values in the event that the option is not found """ + def __init__(self, *args, **kwargs): """Make configuration options case sensitive""" ConfigParser.ConfigParser.__init__(self, *args, **kwargs) @@ -59,7 +62,11 @@ class DefaultConfigParser(ConfigParser.ConfigParser): class Option(object): - def __init__(self, desc, default, cmd=False, odesc=False, + """ a single option, which might be read from the command line, + environment, or config file """ + + # pylint: disable=C0103,R0913 + def __init__(self, desc, default, cmd=None, odesc=False, env=False, cf=False, cook=False, long_arg=False, deprecated_cf=None): self.desc = desc @@ -69,7 +76,7 @@ class Option(object): if not self.long: if cmd and (cmd[0] != '-' or len(cmd) != 2): raise OptionFailure("Poorly formed command %s" % cmd) - elif cmd and (not cmd.startswith('--')): + elif cmd and not cmd.startswith('--'): raise OptionFailure("Poorly formed command %s" % cmd) self.odesc = odesc self.env = env @@ -79,8 +86,13 @@ class Option(object): if not odesc and not cook and isinstance(self.default, bool): self.boolean = True self.cook = cook + self.value = None + # pylint: enable=C0103,R0913 def get_cooked_value(self, value): + """ get the value of this option after performing any option + munging specified in the 'cook' keyword argument to the + constructor """ if self.boolean: return True if self.cook: @@ -112,6 +124,7 @@ class Option(object): return "".join(rv) def buildHelpMessage(self): + """ build the help message for this option """ vals = [] if not self.cmd: return '' @@ -121,11 +134,13 @@ class Option(object): else: vals.append("%s %s" % (self.cmd, self.odesc)) else: - vals.append(self.cmd) + vals.append(self.cmd) vals.append(self.desc) return " %-28s %s\n" % tuple(vals) def buildGetopt(self): + """ build a string suitable for describing this short option + to getopt """ gstr = '' if self.long: return gstr @@ -136,12 +151,18 @@ class Option(object): return gstr def buildLongGetopt(self): + """ build a string suitable for describing this long option to + getopt """ if self.odesc: return self.cmd[2:] + '=' else: return self.cmd[2:] def parse(self, opts, rawopts, configparser=None): + """ parse a single option. try parsing the data out of opts + (the results of getopt), rawopts (the raw option string), the + environment, and finally the config parser. either opts or + rawopts should be provided, but not both """ if self.cmd and opts: # Processing getopted data optinfo = [opt[1] for opt in opts if opt[0] == self.cmd] @@ -170,7 +191,8 @@ class Option(object): pass if self.deprecated_cf: try: - self.value = self.get_cooked_value(configparser.get(*self.deprecated_cf)) + self.value = self.get_cooked_value( + configparser.get(*self.deprecated_cf)) print("Warning: [%s] %s is deprecated, use [%s] %s instead" % (self.deprecated_cf[0], self.deprecated_cf[1], self.cf[0], self.cf[1])) @@ -184,9 +206,13 @@ class Option(object): class OptionSet(dict): + """ a set of Option objects that interfaces with getopt and + DefaultConfigParser to populate a dict of <option name>:<value> + """ + def __init__(self, *args, **kwargs): dict.__init__(self, *args) - self.hm = self.buildHelpMessage() + self.hm = self.buildHelpMessage() # pylint: disable=C0103 if 'configfile' in kwargs: self.cfile = kwargs['configfile'] else: @@ -203,27 +229,34 @@ class OptionSet(dict): if sys.argv[1] == 'init': return else: - print("Warning! Unable to read specified configuration file: %s" - % self.cfile) + print("Warning! Unable to read specified configuration file: " + "%s" % self.cfile) def buildGetopt(self): + """ build a short option description string suitable for use + by getopt.getopt """ return ''.join([opt.buildGetopt() for opt in list(self.values())]) def buildLongGetopt(self): + """ build a list of long options suitable for use by + getopt.getopt """ return [opt.buildLongGetopt() for opt in list(self.values()) if opt.long] def buildHelpMessage(self): + """ Build the help mesage for this option set, or use self.hm + if it is set """ if hasattr(self, 'hm'): return self.hm hlist = [] # list of _non-empty_ help messages for opt in list(self.values()): - hm = opt.buildHelpMessage() - if hm: - hlist.append(hm) + helpmsg = opt.buildHelpMessage() + if helpmsg: + hlist.append(helpmsg) return ''.join(hlist) def helpExit(self, msg='', code=1): + """ print help and exit """ if msg: print(msg) print("Usage:") @@ -231,6 +264,7 @@ class OptionSet(dict): raise SystemExit(code) def versionExit(self, code=0): + """ print the version of bcfg2 and exit """ print("%s %s on Python %s" % (os.path.basename(sys.argv[0]), __version__, @@ -269,42 +303,43 @@ class OptionSet(dict): def list_split(c_string): + """ split an option string on commas, optionally surrounded by + whitespace, returning a list """ if c_string: return re.split("\s*,\s*", c_string) return [] def colon_split(c_string): + """ split an option string on colons, returning a list """ if c_string: return c_string.split(':') return [] -def get_bool(s): +def get_bool(val): + """ given a string value of a boolean configuration option, return + an actual bool (True or False) """ # these values copied from ConfigParser.RawConfigParser.getboolean # with the addition of True and False truelist = ["1", "yes", "True", "true", "on"] falselist = ["0", "no", "False", "false", "off"] - if s in truelist: + if val in truelist: return True - elif s in falselist: + elif val in falselist: return False else: raise ValueError - -""" -Options: - Accepts keyword argument list with the following values: +# Options accepts keyword argument list with the following values: +# default: default value for the option +# cmd: command line switch +# odesc: option description +# cf: tuple containing section/option +# cook: method for parsing option +# long_arg: (True|False) specifies whether cmd is a long argument - default: default value for the option - cmd: command line switch - odesc: option description - cf: tuple containing section/option - cook: method for parsing option - long_arg: (True|False) specifies whether cmd is a long argument -""" # General options CFILE = \ Option('Specify configuration file', @@ -428,7 +463,8 @@ SERVER_REPOSITORY = \ SERVER_PLUGINS = \ Option('Server plugin list', # default server plugins - default=['Bundler', 'Cfg', 'Metadata', 'Pkgmgr', 'Rules', 'SSHbase'], + default=['Bundler', 'Cfg', 'Metadata', 'Pkgmgr', 'Rules', + 'SSHbase'], cf=('server', 'plugins'), cook=list_split) SERVER_MCONNECT = \ @@ -444,7 +480,7 @@ SERVER_FILEMONITOR = \ SERVER_FAM_IGNORE = \ Option('File globs to ignore', default=['*~', '*#', '.#*', '*.swp', '.*.swx', 'SCCS', '.svn', - '4913', '.gitignore',], + '4913', '.gitignore'], cf=('server', 'ignore_files'), cook=list_split) SERVER_LISTEN_ALL = \ @@ -1066,13 +1102,18 @@ class OptionParser(OptionSet): argv = sys.argv[1:] # the bootstrap is always quiet, since it's running with a # default config file and so might produce warnings otherwise - self.Bootstrap = OptionSet([('configfile', CFILE)], quiet=True) - self.Bootstrap.parse(argv, do_getopt=False) - OptionSet.__init__(self, args, configfile=self.Bootstrap['configfile'], + self.bootstrap = OptionSet([('configfile', CFILE)], quiet=True) + self.bootstrap.parse(argv, do_getopt=False) + OptionSet.__init__(self, args, configfile=self.bootstrap['configfile'], quiet=quiet) self.optinfo = copy.copy(args) + # these will be set by parse() and then used by reparse() + self.argv = [] + self.do_getopt = True def reparse(self): + """ parse the options again, taking any changes (e.g., to the + config file) into account """ for key, opt in self.optinfo.items(): self[key] = opt if "args" not in self.optinfo: @@ -1085,6 +1126,7 @@ class OptionParser(OptionSet): OptionSet.parse(self, self.argv, do_getopt=self.do_getopt) def add_option(self, name, opt): + """ Add an option to the parser """ self[name] = opt self.optinfo[name] = opt diff --git a/src/lib/Bcfg2/Server/FileMonitor/Fam.py b/src/lib/Bcfg2/Server/FileMonitor/Fam.py index aef74add4..9c6031be9 100644 --- a/src/lib/Bcfg2/Server/FileMonitor/Fam.py +++ b/src/lib/Bcfg2/Server/FileMonitor/Fam.py @@ -7,21 +7,24 @@ import logging from time import time from Bcfg2.Server.FileMonitor import FileMonitor -logger = logging.getLogger(__name__) +LOGGER = logging.getLogger(__name__) + class Fam(FileMonitor): + """ file monitor with support for FAM """ + __priority__ = 90 def __init__(self, ignore=None, debug=False): FileMonitor.__init__(self, ignore=ignore, debug=debug) - self.fm = _fam.open() + self.filemonitor = _fam.open() self.users = {} def fileno(self): """Return fam file handle number.""" - return self.fm.fileno() + return self.filemonitor.fileno() - def handle_event_set(self, _): + def handle_event_set(self, _=None): self.Service() def handle_events_in_interval(self, interval): @@ -30,13 +33,13 @@ class Fam(FileMonitor): if self.Service(): now = time() - def AddMonitor(self, path, obj): + def AddMonitor(self, path, obj, _=None): """Add a monitor to path, installing a callback to obj.HandleEvent.""" mode = os.stat(path)[stat.ST_MODE] if stat.S_ISDIR(mode): - handle = self.fm.monitorDirectory(path, None) + handle = self.filemonitor.monitorDirectory(path, None) else: - handle = self.fm.monitorFile(path, None) + handle = self.filemonitor.monitorFile(path, None) self.handles[handle.requestID()] = handle if obj != None: self.users[handle.requestID()] = obj @@ -50,10 +53,10 @@ class Fam(FileMonitor): start = time() now = time() while (time() - now) < interval: - if self.fm.pending(): - while self.fm.pending(): + if self.filemonitor.pending(): + while self.filemonitor.pending(): count += 1 - rawevents.append(self.fm.nextEvent()) + rawevents.append(self.filemonitor.nextEvent()) now = time() unique = [] bookkeeping = [] @@ -73,10 +76,10 @@ class Fam(FileMonitor): if event.requestID in self.users: try: self.users[event.requestID].HandleEvent(event) - except: - logger.error("Handling event for file %s" % event.filename, + except: # pylint: disable=W0702 + LOGGER.error("Handling event for file %s" % event.filename, exc_info=1) end = time() - logger.info("Processed %s fam events in %03.03f seconds. %s coalesced" % - (count, (end - start), collapsed)) + LOGGER.info("Processed %s fam events in %03.03f seconds. " + "%s coalesced" % (count, (end - start), collapsed)) return count diff --git a/src/lib/Bcfg2/Server/FileMonitor/Gamin.py b/src/lib/Bcfg2/Server/FileMonitor/Gamin.py index 9d4330e89..d0ba59cd8 100644 --- a/src/lib/Bcfg2/Server/FileMonitor/Gamin.py +++ b/src/lib/Bcfg2/Server/FileMonitor/Gamin.py @@ -2,15 +2,12 @@ import os import stat -import logging # pylint: disable=F0401 from gamin import WatchMonitor, GAMCreated, GAMExists, GAMEndExist, \ GAMChanged, GAMDeleted # pylint: enable=F0401 from Bcfg2.Server.FileMonitor import Event, FileMonitor -logger = logging.getLogger(__name__) - class GaminEvent(Event): """ @@ -28,6 +25,7 @@ class GaminEvent(Event): class Gamin(FileMonitor): + """ file monitor with gamin support """ __priority__ = 10 def __init__(self, ignore=None, debug=False): diff --git a/src/lib/Bcfg2/Server/FileMonitor/Inotify.py b/src/lib/Bcfg2/Server/FileMonitor/Inotify.py index 75eff3bc5..5a8a1e1c6 100644 --- a/src/lib/Bcfg2/Server/FileMonitor/Inotify.py +++ b/src/lib/Bcfg2/Server/FileMonitor/Inotify.py @@ -1,17 +1,18 @@ """ Inotify driver for file alteration events """ import os -import sys import logging import pyinotify # pylint: disable=F0401 -from Bcfg2.Compat import reduce +from Bcfg2.Compat import reduce # pylint: disable=W0622 from Bcfg2.Server.FileMonitor import Event from Bcfg2.Server.FileMonitor.Pseudo import Pseudo -logger = logging.getLogger(__name__) +LOGGER = logging.getLogger(__name__) class Inotify(Pseudo, pyinotify.ProcessEvent): + """ file monitor with inotify support """ + __priority__ = 1 # pylint: disable=E1101 action_map = {pyinotify.IN_CREATE: 'created', @@ -24,18 +25,18 @@ class Inotify(Pseudo, pyinotify.ProcessEvent): def __init__(self, ignore=None, debug=False): Pseudo.__init__(self, ignore=ignore, debug=debug) - pyinotify.ProcessEvent(self) + pyinotify.ProcessEvent.__init__(self) self.event_filter = dict() self.watches_by_path = dict() # these are created in start() after the server is done forking self.notifier = None - self.wm = None + self.watchmgr = None self.add_q = [] def start(self): Pseudo.start(self) - self.wm = pyinotify.WatchManager() - self.notifier = pyinotify.ThreadedNotifier(self.wm, self) + self.watchmgr = pyinotify.WatchManager() + self.notifier = pyinotify.ThreadedNotifier(self.watchmgr, self) self.notifier.start() for monitor in self.add_q: self.AddMonitor(*monitor) @@ -43,7 +44,7 @@ class Inotify(Pseudo, pyinotify.ProcessEvent): def fileno(self): if self.started: - return self.wm.get_fd() + return self.watchmgr.get_fd() else: return None @@ -54,9 +55,9 @@ class Inotify(Pseudo, pyinotify.ProcessEvent): action = aname break try: - watch = self.wm.watches[ievent.wd] + watch = self.watchmgr.watches[ievent.wd] except KeyError: - logger.error("Error handling event for %s: Watch %s not found" % + LOGGER.error("Error handling event for %s: Watch %s not found" % (ievent.pathname, ievent.wd)) return # FAM-style file monitors return the full path to the parent @@ -87,7 +88,7 @@ class Inotify(Pseudo, pyinotify.ProcessEvent): ievent.pathname in self.event_filter[ievent.wd]): self.events.append(evt) - def AddMonitor(self, path, obj): + def AddMonitor(self, path, obj, handleID=None): # strip trailing slashes path = path.rstrip("/") @@ -116,18 +117,18 @@ class Inotify(Pseudo, pyinotify.ProcessEvent): # see if this path is already being watched try: - wd = self.watches_by_path[watch_path] + watchdir = self.watches_by_path[watch_path] except KeyError: - wd = self.wm.add_watch(watch_path, self.mask, - quiet=False)[watch_path] - self.watches_by_path[watch_path] = wd + watchdir = self.watchmgr.add_watch(watch_path, self.mask, + quiet=False)[watch_path] + self.watches_by_path[watch_path] = watchdir produce_exists = True if not is_dir: - if wd not in self.event_filter: - self.event_filter[wd] = [path] - elif path not in self.event_filter[wd]: - self.event_filter[wd].append(path) + if watchdir not in self.event_filter: + self.event_filter[watchdir] = [path] + elif path not in self.event_filter[watchdir]: + self.event_filter[watchdir].append(path) else: # we've been asked to watch a file that we're already # watching, so we don't need to produce 'exists' diff --git a/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py b/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py index 089d4cf0f..9062cbfd8 100644 --- a/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py +++ b/src/lib/Bcfg2/Server/FileMonitor/Pseudo.py @@ -1,12 +1,13 @@ """ Pseudo provides static monitor support for file alteration events """ import os -import logging from Bcfg2.Server.FileMonitor import FileMonitor, Event -logger = logging.getLogger(__name__) class Pseudo(FileMonitor): + """ file monitor that only produces events on server startup and + doesn't actually monitor at all """ + __priority__ = 99 def AddMonitor(self, path, obj, handleID=None): @@ -15,9 +16,9 @@ class Pseudo(FileMonitor): handleID = len(list(self.handles.keys())) self.events.append(Event(handleID, path, 'exists')) if os.path.isdir(path): - dirList = os.listdir(path) - for includedFile in dirList: - self.events.append(Event(handleID, includedFile, 'exists')) + dirlist = os.listdir(path) + for fname in dirlist: + self.events.append(Event(handleID, fname, 'exists')) self.events.append(Event(handleID, path, 'endExist')) if obj != None: diff --git a/src/lib/Bcfg2/Server/FileMonitor/__init__.py b/src/lib/Bcfg2/Server/FileMonitor/__init__.py index fd0cb66f1..1b12ab703 100644 --- a/src/lib/Bcfg2/Server/FileMonitor/__init__.py +++ b/src/lib/Bcfg2/Server/FileMonitor/__init__.py @@ -4,12 +4,13 @@ import os import sys import fnmatch import logging -import pkgutil from time import sleep, time -logger = logging.getLogger(__name__) +LOGGER = logging.getLogger(__name__) + class Event(object): + """ Base class for all FAM events """ def __init__(self, request_id, filename, code): self.requestID = request_id self.filename = filename @@ -53,33 +54,40 @@ class FileMonitor(object): self.started = True def debug_log(self, msg): + """ log a debug message """ if self.debug: - logger.info(msg) + LOGGER.info(msg) def should_ignore(self, event): + """ returns true if an event should be ignored """ for pattern in self.ignore: - if (fnmatch.fnmatch(event.filename, pattern) or + if (fnmatch.fnmatch(event.filename, pattern) or fnmatch.fnmatch(os.path.split(event.filename)[-1], pattern)): self.debug_log("Ignoring %s" % event) return True return False def pending(self): + """ returns True if there are pending events """ return bool(self.events) def get_event(self): + """ get the oldest pending event """ return self.events.pop(0) def fileno(self): + """ get the file descriptor of the file monitor thread """ return 0 def handle_one_event(self, event): + """ handle the given event by dispatching it to the object + that handles events for the path """ if not self.started: self.start() if self.should_ignore(event): return if event.requestID not in self.handles: - logger.info("Got event for unexpected id %s, file %s" % + LOGGER.info("Got event for unexpected id %s, file %s" % (event.requestID, event.filename)) return self.debug_log("Dispatching event %s %s to obj %s" % @@ -87,12 +95,13 @@ class FileMonitor(object): self.handles[event.requestID])) try: self.handles[event.requestID].HandleEvent(event) - except: + except: # pylint: disable=W0702 err = sys.exc_info()[1] - logger.error("Error in handling of event %s for %s: %s" % + LOGGER.error("Error in handling of event %s for %s: %s" % (event.code2str(), event.filename, err)) def handle_event_set(self, lock=None): + """ Handle all pending events """ if not self.started: self.start() count = 1 @@ -100,19 +109,18 @@ class FileMonitor(object): start = time() if lock: lock.acquire() - try: - self.handle_one_event(event) - while self.pending(): - self.handle_one_event(self.get_event()) - count += 1 - except: - pass + self.handle_one_event(event) + while self.pending(): + self.handle_one_event(self.get_event()) + count += 1 if lock: lock.release() end = time() - logger.info("Handled %d events in %.03fs" % (count, (end - start))) + LOGGER.info("Handled %d events in %.03fs" % (count, (end - start))) def handle_events_in_interval(self, interval): + """ handle events for the specified period of time (in + seconds) """ if not self.started: self.start() end = time() + interval @@ -124,10 +132,15 @@ class FileMonitor(object): sleep(0.5) def shutdown(self): + """ shutdown the monitor """ self.started = False + def AddMonitor(self, path, obj, handleID=None): + """ watch the specified path, alerting obj to events """ + raise NotImplementedError + -available = dict() +available = dict() # pylint: disable=C0103 # todo: loading the monitor drivers should be automatic from Bcfg2.Server.FileMonitor.Pseudo import Pseudo diff --git a/src/lib/Bcfg2/Server/Lint/__init__.py b/src/lib/Bcfg2/Server/Lint/__init__.py index eea205b75..a59214048 100644 --- a/src/lib/Bcfg2/Server/Lint/__init__.py +++ b/src/lib/Bcfg2/Server/Lint/__init__.py @@ -10,6 +10,9 @@ import fcntl import termios import struct from Bcfg2.Server import XI_NAMESPACE +from Bcfg2.Compat import walk_packages + +__all__ = [m[1] for m in walk_packages(path=__path__)] def _ioctl_GWINSZ(fd): # pylint: disable=C0103 @@ -99,6 +102,7 @@ class Plugin(object): class ErrorHandler (object): """ a class to handle errors for bcfg2-lint plugins """ + def __init__(self, config=None): self.errors = 0 self.warnings = 0 @@ -114,32 +118,26 @@ class ErrorHandler (object): else: self._wrapper = lambda s: [s] - self._handlers = {} + self.errors = dict() if config is not None: - for err, action in config.items(): - if "warn" in action: - self._handlers[err] = self.warn - elif "err" in action: - self._handlers[err] = self.error - else: - self._handlers[err] = self.debug + self.RegisterErrors(config.items()) 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 err not in self.errors: if "warn" in action: - self._handlers[err] = self.warn + self.errors[err] = self.warn elif "err" in action: - self._handlers[err] = self.error + self.errors[err] = self.error else: - self._handlers[err] = self.debug + self.errors[err] = self.debug def dispatch(self, err, msg): """ Dispatch an error to the correct handler """ - if err in self._handlers: - self._handlers[err](msg) + if err in self.errors: + self.errors[err](msg) self.logger.debug(" (%s)" % err) else: # assume that it's an error, but complain diff --git a/src/lib/Bcfg2/Server/Plugins/Metadata.py b/src/lib/Bcfg2/Server/Plugins/Metadata.py index 468d1f190..477f88b82 100644 --- a/src/lib/Bcfg2/Server/Plugins/Metadata.py +++ b/src/lib/Bcfg2/Server/Plugins/Metadata.py @@ -14,20 +14,20 @@ import Bcfg2.Server import Bcfg2.Server.Lint import Bcfg2.Server.Plugin import Bcfg2.Server.FileMonitor -from Bcfg2.Compat import MutableMapping, all # pylint: disable=W0622 +from Bcfg2.Compat import MutableMapping, all, wraps # pylint: disable=W0622 from Bcfg2.version import Bcfg2VersionInfo try: from django.db import models - has_django = True + HAS_DJANGO = True except ImportError: - has_django = False + HAS_DJANGO = False -logger = logging.getLogger(__name__) +LOGGER = logging.getLogger(__name__) def locked(fd): - """Aquire a lock on a file""" + """ Acquire a lock on a file """ try: fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) except IOError: @@ -35,13 +35,17 @@ def locked(fd): return False -if has_django: +if HAS_DJANGO: class MetadataClientModel(models.Model, Bcfg2.Server.Plugin.PluginDatabaseModel): + """ django model for storing clients in the database """ hostname = models.CharField(max_length=255, primary_key=True) version = models.CharField(max_length=31, null=True) - class ClientVersions(MutableMapping): + class ClientVersions(MutableMapping, object): + """ dict-like object to make it easier to access client bcfg2 + versions from the database """ + def __getitem__(self, key): try: return MetadataClientModel.objects.get(hostname=key).version @@ -77,7 +81,7 @@ if has_django: def __contains__(self, key): try: - client = MetadataClientModel.objects.get(hostname=key) + MetadataClientModel.objects.get(hostname=key) return True except MetadataClientModel.DoesNotExist: return False @@ -85,6 +89,7 @@ if has_django: class XMLMetadataConfig(Bcfg2.Server.Plugin.XMLFileBacked): """Handles xml config files and all XInclude statements""" + def __init__(self, metadata, watch_clients, basefile): # we tell XMLFileBacked _not_ to add a monitor for this file, # because the main Metadata plugin has already added one. @@ -105,18 +110,23 @@ class XMLMetadataConfig(Bcfg2.Server.Plugin.XMLFileBacked): Bcfg2.Server.FileMonitor.Pseudo) def _get_xdata(self): + """ getter for xdata property """ if not self.data: raise Bcfg2.Server.Plugin.MetadataRuntimeError("%s has no data" % self.basefile) return self.data def _set_xdata(self, val): + """ setter for xdata property. in practice this should only be + used by the test suite """ self.data = val xdata = property(_get_xdata, _set_xdata) @property def base_xdata(self): + """ property to get the data of the base file (without any + xincludes processed) """ if not self.basedata: raise Bcfg2.Server.Plugin.MetadataRuntimeError("%s has no data" % self.basefile) @@ -160,7 +170,6 @@ class XMLMetadataConfig(Bcfg2.Server.Plugin.XMLFileBacked): newcontents = lxml.etree.tostring(dataroot, xml_declaration=False, pretty_print=True).decode('UTF-8') - fd = datafile.fileno() while locked(fd) == True: pass @@ -198,7 +207,7 @@ class XMLMetadataConfig(Bcfg2.Server.Plugin.XMLFileBacked): 'xmltree': self.basedata, 'xquery': cli} else: - """Try to find the data in included files""" + # Try to find the data in included files for included in self.extras: try: xdata = lxml.etree.parse(included, @@ -217,7 +226,7 @@ class XMLMetadataConfig(Bcfg2.Server.Plugin.XMLFileBacked): if self.fam and self.should_monitor: self.fam.AddMonitor(fpath, self.metadata) - def HandleEvent(self, event): + def HandleEvent(self, event=None): """Handle fam events""" filename = os.path.basename(event.filename) if event.filename in self.extras: @@ -233,7 +242,8 @@ class XMLMetadataConfig(Bcfg2.Server.Plugin.XMLFileBacked): class ClientMetadata(object): """This object contains client metadata.""" - def __init__(self, client, profile, groups, bundles, aliases, addresses, + # pylint: disable=R0913 + def __init__(self, client, profile, groups, bundles, aliases, addresses, categories, uuid, password, version, query): self.hostname = client self.profile = profile @@ -248,15 +258,18 @@ class ClientMetadata(object): self.version = version try: self.version_info = Bcfg2VersionInfo(version) - except: + except (ValueError, AttributeError): self.version_info = None self.query = query + # pylint: enable=R0913 def inGroup(self, group): """Test to see if client is a member of group.""" return group in self.groups def group_in_category(self, category): + """ return the group in the given category that the client is + a member of, or the empty string """ for grp in self.query.all_groups_in_category(category): if grp in self.groups: return grp @@ -264,6 +277,9 @@ class ClientMetadata(object): class MetadataQuery(object): + """ object supplied to client metadata to allow client metadata + objects to query metadata without being able to modify it """ + def __init__(self, by_name, get_clients, by_groups, by_profiles, all_groups, all_groups_in_category): # resolver is set later @@ -275,37 +291,52 @@ class MetadataQuery(object): self.all_groups_in_category = all_groups_in_category def _warn_string(self, func): - # it's a common mistake to call by_groups, etc., in templates with - # a single string argument instead of a list. that doesn't cause - # errors because strings are iterables. this decorator warns - # about that usage. + """ decorator to warn that a MetadataQuery function that + expects a list has been called with a single string argument + instead. this is a common mistake in templates, and it + doesn't cause errors because strings are iterables """ + + # pylint: disable=C0111 + @wraps(func) def inner(arg): if isinstance(arg, str): - logger.warning("%s: %s takes a list as argument, not a string" % - (self.__class__.__name__, func.__name__)) + LOGGER.warning("%s: %s takes a list as argument, not a string" + % (self.__class__.__name__, func.__name__)) return func(arg) + # pylint: enable=C0111 + return inner def by_groups(self, groups): + """ get a list of ClientMetadata objects that are in all given + groups """ # don't need to decorate this with _warn_string because # names_by_groups is decorated return [self.by_name(name) for name in self.names_by_groups(groups)] def by_profiles(self, profiles): + """ get a list of ClientMetadata objects that are in any of + the given profiles """ # don't need to decorate this with _warn_string because # names_by_profiles is decorated - return [self.by_name(name) for name in self.names_by_profiles(profiles)] + return [self.by_name(name) + for name in self.names_by_profiles(profiles)] def all(self): + """ get a list of all ClientMetadata objects """ return [self.by_name(name) for name in self.all_clients()] class MetadataGroup(tuple): + """ representation of a metadata group. basically just a named tuple """ + + # pylint: disable=R0913,W0613 def __new__(cls, name, bundles=None, category=None, is_profile=False, is_public=False, is_private=False): if bundles is None: bundles = set() return tuple.__new__(cls, (bundles, category)) + # pylint: enable=W0613 def __init__(self, name, bundles=None, category=None, is_profile=False, is_public=False, is_private=False): @@ -320,6 +351,7 @@ class MetadataGroup(tuple): self.is_private = is_private # record which clients we've warned about category suppression self.warned = [] + # pylint: enable=R0913 def __str__(self): return repr(self) @@ -347,7 +379,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, self.watch_clients = watch_clients self.states = dict() self.extra = dict() - self.handlers = [] + self.handlers = dict() self.groups_xml = self._handle_file("groups.xml") if (self._use_db and os.path.exists(os.path.join(self.data, "clients.xml"))): @@ -407,6 +439,8 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, "w").write(kwargs[aname]) def _handle_file(self, fname): + """ set up the necessary magic for handling a metadata file + (clients.xml or groups.xml, e.g.) """ if self.watch_clients: try: self.core.fam.AddMonitor(os.path.join(self.data, fname), self) @@ -416,13 +450,15 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, self.logger.error(msg) raise Bcfg2.Server.Plugin.PluginInitError(msg) self.states[fname] = False - aname = re.sub(r'[^A-z0-9_]', '_', fname) xmlcfg = XMLMetadataConfig(self, self.watch_clients, fname) - self.handlers.append(xmlcfg.HandleEvent) + aname = re.sub(r'[^A-z0-9_]', '_', os.path.basename(fname)) + self.handlers[xmlcfg.HandleEvent] = getattr(self, + "_handle_%s_event" % aname) self.extra[fname] = [] return xmlcfg def _search_xdata(self, tag, name, tree, alias=False): + """ Generic method to find XML data (group, client, etc.) """ for node in tree.findall("//%s" % tag): if node.get("name") == name: return node @@ -442,9 +478,11 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, return self._search_xdata("Bundle", bundle_name, tree) def search_client(self, client_name, tree): + """ find a client in the given XML tree """ return self._search_xdata("Client", client_name, tree, alias=True) def _add_xdata(self, config, tag, name, attribs=None, alias=False): + """ Generic method to add XML data (group, client, etc.) """ node = self._search_xdata(tag, name, config.xdata, alias=alias) if node != None: self.logger.error("%s \"%s\" already exists" % (tag, name)) @@ -491,6 +529,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, attribs=attribs, alias=True) def _update_xdata(self, config, tag, name, attribs, alias=False): + """ Generic method to modify XML data (group, client, etc.) """ node = self._search_xdata(tag, name, config.xdata, alias=alias) if node == None: self.logger.error("%s \"%s\" does not exist" % (tag, name)) @@ -534,7 +573,8 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, else: return self.clients - def _remove_xdata(self, config, tag, name, alias=False): + def _remove_xdata(self, config, tag, name): + """ Generic method to remove XML data (group, client, etc.) """ node = self._search_xdata(tag, name, config.xdata) if node == None: self.logger.error("%s \"%s\" does not exist" % (tag, name)) @@ -561,10 +601,10 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, def remove_bundle(self, bundle_name): """Remove a bundle.""" if self._use_db: - msg = "Metadata does not support removing bundles with " + \ - "use_database enabled" - self.logger.error(msg) - raise Bcfg2.Server.Plugin.PluginExecutionError(msg) + msg = "Metadata does not support removing bundles with " + \ + "use_database enabled" + self.logger.error(msg) + raise Bcfg2.Server.Plugin.PluginExecutionError(msg) else: return self._remove_xdata(self.groups_xml, "Bundle", bundle_name) @@ -582,7 +622,9 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, else: return self._remove_xdata(self.clients_xml, "Client", client_name) - def _handle_clients_xml_event(self, event): + def _handle_clients_xml_event(self, _): + """ handle all events for clients.xml and files xincluded from + clients.xml """ xdata = self.clients_xml.xdata self.clients = [] self.clientgroups = {} @@ -640,12 +682,16 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, if self._use_db: self.clients = self.list_clients() - def _handle_groups_xml_event(self, event): + def _handle_groups_xml_event(self, _): # pylint: disable=R0912 + """ re-read groups.xml on any event on it """ self.groups = {} # these three functions must be separate functions in order to # ensure that the scope is right for the closures they return def get_condition(element): + """ Return a predicate that returns True if a client meets + the condition specified in the given Group or Client + element """ negate = element.get('negate', 'false').lower() == 'true' pname = element.get("name") if element.tag == 'Group': @@ -654,7 +700,13 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, return lambda c, g, _: negate != (pname == c) def get_category_condition(category, gname): - def in_cat(client, groups, categories): + """ get a predicate that returns False if a client is + already a member of a group in the given category, True + otherwise """ + def in_cat(client, groups, categories): # pylint: disable=W0613 + """ return True if the client is already a member of a + group in the category given in the enclosing function, + False otherwise """ if category in categories: if (gname not in self.groups or client not in self.groups[gname].warned): @@ -670,6 +722,8 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, return in_cat def aggregate_conditions(conditions): + """ aggregate all conditions on a given group declaration + into a single predicate """ return lambda client, groups, cats: \ all(cond(client, groups, cats) for cond in conditions) @@ -735,14 +789,12 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, def HandleEvent(self, event): """Handle update events for data files.""" - for hdlr in self.handlers: - aname = re.sub(r'[^A-z0-9_]', '_', - os.path.basename(event.filename)) - if hdlr(event): + for handles, event_handler in self.handlers.items(): + if handles(event): # clear the entire cache when we get an event for any # metadata file self.core.metadata_cache.expire() - getattr(self, "_handle_%s_event" % aname)(event) + event_handler(event) if False not in list(self.states.values()) and self.debug_flag: # check that all groups are real and complete. this is @@ -759,18 +811,19 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, self.debug_log("Group %s set as nonexistent group %s" % (gname, group)) - def set_profile(self, client, profile, addresspair, force=False): + def set_profile(self, client, profile, addresspair): """Set group parameter for provided client.""" self.logger.info("Asserting client %s profile to %s" % (client, profile)) if False in list(self.states.values()): - raise Bcfg2.Server.Plugin.MetadataRuntimeError("Metadata has not been read yet") - if not force and profile not in self.groups: + raise Bcfg2.Server.Plugin.MetadataRuntimeError("Metadata has not " + "been read yet") + if profile not in self.groups: msg = "Profile group %s does not exist" % profile self.logger.error(msg) raise Bcfg2.Server.Plugin.MetadataConsistencyError(msg) group = self.groups[profile] - if not force and not group.is_public: + if not group.is_public: msg = "Cannot set client %s to private group %s" % (client, profile) self.logger.error(msg) @@ -788,8 +841,8 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, (client, profiles, profile)) self.update_client(client, dict(profile=profile)) if client in self.clientgroups: - for p in profiles: - self.clientgroups[client].remove(p) + for prof in profiles: + self.clientgroups[client].remove(prof) self.clientgroups[client].append(profile) else: self.clientgroups[client] = [profile] @@ -872,7 +925,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, """ set group membership based on the contents of groups.xml and initial group membership of this client. Returns a tuple of (allgroups, categories)""" - numgroups = -1 # force one initial pass + numgroups = -1 # force one initial pass if categories is None: categories = dict() while numgroups != len(groups): @@ -893,10 +946,11 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, del categories[group.category] return (groups, categories) - def get_initial_metadata(self, client): + def get_initial_metadata(self, client): # pylint: disable=R0914,R0912 """Return the metadata for a given client.""" if False in list(self.states.values()): - raise Bcfg2.Server.Plugin.MetadataRuntimeError("Metadata has not been read yet") + raise Bcfg2.Server.Plugin.MetadataRuntimeError("Metadata has not " + "been read yet") client = client.lower() if client in self.core.metadata_cache: @@ -904,7 +958,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, if client in self.aliases: client = self.aliases[client] - + groups = set() categories = dict() profile = None @@ -917,7 +971,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, pgroup = self.default if pgroup: - self.set_profile(client, pgroup, (None, None), force=True) + self.set_profile(client, pgroup, (None, None)) groups.add(pgroup) category = self.groups[pgroup].category if category: @@ -958,8 +1012,8 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, try: bundles.update(self.groups[group].bundles) except KeyError: - self.logger.warning("%s: %s is a member of undefined group %s" % - (self.name, client, group)) + self.logger.warning("%s: %s is a member of undefined group %s" + % (self.name, client, group)) aliases = self.raliases.get(client, set()) addresses = self.raddresses.get(client, set()) @@ -989,6 +1043,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, return rv def get_all_group_names(self): + """ return a list of all group names """ all_groups = set() all_groups.update(self.groups.keys()) all_groups.update([g.name for g in self.group_membership.values()]) @@ -998,10 +1053,12 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, return all_groups def get_all_groups_in_category(self, category): + """ return a list of names of groups in the given category """ return set([g.name for g in self.groups.values() if g.category == category]) def get_client_names_by_profiles(self, profiles): + """ return a list of names of clients in the given profile groups """ rv = [] for client in list(self.clients): mdata = self.get_initial_metadata(client) @@ -1010,13 +1067,14 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, return rv def get_client_names_by_groups(self, groups): - mdata = [self.core.build_metadata(client) - for client in list(self.clients)] + """ return a list of names of clients in the given groups """ + mdata = [self.core.build_metadata(client) for client in self.clients] return [md.hostname for md in mdata if md.groups.issuperset(groups)] def get_client_names_by_bundles(self, bundles): - mdata = [self.core.build_metadata(client) - for client in list(self.clients.keys())] + """ given a list of bundles, return a list of names of clients + that use those bundles """ + mdata = [self.core.build_metadata(client) for client in self.clients] return [md.hostname for md in mdata if md.bundles.issuperset(bundles)] def merge_additional_groups(self, imd, groups): @@ -1071,6 +1129,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, self.logger.error("Resolved to %s" % resolved) return False + # pylint: disable=R0911,R0912 def AuthenticateConnection(self, cert, user, password, address): """This function checks auth creds.""" if not isinstance(user, str): @@ -1121,8 +1180,8 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, if client not in self.passwords: if client in self.secure: - self.logger.error("Client %s in secure mode but has no password" - % address[0]) + self.logger.error("Client %s in secure mode but has no " + "password" % address[0]) return False if password != self.password: self.logger.error("Client %s used incorrect global password" % @@ -1147,9 +1206,11 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, if user != 'root': self.session_cache[address] = (time.time(), client) return True + # pylint: enable=R0911,R0912 def process_statistics(self, meta, _): - """Hook into statistics interface to toggle clients in bootstrap mode.""" + """ Hook into statistics interface to toggle clients in + bootstrap mode """ client = meta.hostname if client in self.auth and self.auth[client] == 'bootstrap': self.update_client(client, dict(auth='cert')) @@ -1159,23 +1220,7 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, if only_client: clientmeta = self.core.build_metadata(only_client) - def include_client(client): - return not only_client or client != only_client - - def include_bundle(bundle): - return not only_client or bundle in clientmeta.bundles - - def include_group(group): - return not only_client or group in clientmeta.groups - - groups_tree = lxml.etree.parse(os.path.join(self.data, "groups.xml"), - parser=Bcfg2.Server.XMLParser) - try: - groups_tree.xinclude() - except lxml.etree.XIncludeError: - self.logger.error("Failed to process XInclude for groups.xml: %s" % - sys.exc_info()[1]) - groups = groups_tree.getroot() + groups = self.groups_xml.xdata.getroot() categories = {'default': 'grey83'} viz_str = [] egroups = groups.findall("Group") + groups.findall('.//Groups/Group') @@ -1186,37 +1231,72 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, if None in categories: del categories[None] if hosts: - instances = {} - for client in list(self.clients): - if include_client(client): - continue - if client in self.clientgroups: - groups = self.clientgroups[client] - elif self.default: - groups = [self.default] - else: - continue - for group in groups: - try: - instances[group].append(client) - except KeyError: - instances[group] = [client] - for group, clist in list(instances.items()): - clist.sort() - viz_str.append('"%s-instances" [ label="%s", shape="record" ];' % - (group, '|'.join(clist))) - viz_str.append('"%s-instances" -> "group-%s";' % - (group, group)) + viz_str.extend(self._viz_hosts(only_client)) if bundles: - bundles = [] - [bundles.append(bund.get('name')) \ - for bund in groups.findall('.//Bundle') \ - if bund.get('name') not in bundles \ - and include_bundle(bund.get('name'))] - bundles.sort() - for bundle in bundles: - viz_str.append('"bundle-%s" [ label="%s", shape="septagon"];' % - (bundle, bundle)) + viz_str.extend(self._viz_bundles(bundles, clientmeta)) + viz_str.extend(self._viz_groups(egroups, bundles, clientmeta)) + if key: + for category in categories: + viz_str.append('"%s" [label="%s", shape="record", ' + 'style="filled", fillcolor="%s"];' % + (category, category, categories[category])) + return "\n".join("\t" + s for s in viz_str) + + def _viz_hosts(self, only_client): + """ add hosts to the viz graph """ + def include_client(client): + """ return True if the given client should be included in + the graph""" + return not only_client or client != only_client + + instances = {} + rv = [] + for client in list(self.clients): + if include_client(client): + continue + if client in self.clientgroups: + grps = self.clientgroups[client] + elif self.default: + grps = [self.default] + else: + continue + for group in grps: + try: + instances[group].append(client) + except KeyError: + instances[group] = [client] + for group, clist in list(instances.items()): + clist.sort() + rv.append('"%s-instances" [ label="%s", shape="record" ];' % + (group, '|'.join(clist))) + rv.append('"%s-instances" -> "group-%s";' % (group, group)) + return rv + + def _viz_bundles(self, bundles, clientmeta): + """ add bundles to the viz graph """ + + def include_bundle(bundle): + """ return True if the given bundle should be included in + the graph""" + return not clientmeta or bundle in clientmeta.bundles + + bundles = list(set(bund.get('name')) + for bund in self.groups_xml.xdata.findall('.//Bundle') + if include_bundle(bund.get('name'))) + bundles.sort() + return ['"bundle-%s" [ label="%s", shape="septagon"];' % (bundle, + bundle) + for bundle in bundles] + + def _viz_groups(self, egroups, bundles, clientmeta): + """ add groups to the viz graph """ + + def include_group(group): + """ return True if the given group should be included in + the graph """ + return not clientmeta or group in clientmeta.groups + + rv = [] gseen = [] for group in egroups: if group.get('profile', 'false') == 'true': @@ -1225,31 +1305,29 @@ class Metadata(Bcfg2.Server.Plugin.Metadata, style = "filled" gseen.append(group.get('name')) if include_group(group.get('name')): - viz_str.append('"group-%s" [label="%s", style="%s", fillcolor=%s];' % - (group.get('name'), group.get('name'), style, - group.get('color'))) + rv.append('"group-%s" [label="%s", style="%s", fillcolor=%s];' + % (group.get('name'), group.get('name'), style, + group.get('color'))) if bundles: for bundle in group.findall('Bundle'): - viz_str.append('"group-%s" -> "bundle-%s";' % - (group.get('name'), bundle.get('name'))) + rv.append('"group-%s" -> "bundle-%s";' % + (group.get('name'), bundle.get('name'))) gfmt = '"group-%s" [label="%s", style="filled", fillcolor="grey83"];' for group in egroups: for parent in group.findall('Group'): - if parent.get('name') not in gseen and include_group(parent.get('name')): - viz_str.append(gfmt % (parent.get('name'), - parent.get('name'))) + if (parent.get('name') not in gseen and + include_group(parent.get('name'))): + rv.append(gfmt % (parent.get('name'), + parent.get('name'))) gseen.append(parent.get("name")) if include_group(group.get('name')): - viz_str.append('"group-%s" -> "group-%s";' % - (group.get('name'), parent.get('name'))) - if key: - for category in categories: - viz_str.append('"%s" [label="%s", shape="record", style="filled", fillcolor="%s"];' % - (category, category, categories[category])) - return "\n".join("\t" + s for s in viz_str) + rv.append('"group-%s" -> "group-%s";' % + (group.get('name'), parent.get('name'))) class MetadataLint(Bcfg2.Server.Lint.ServerPlugin): + """ bcfg2-lint plugin for Metadata """ + def Run(self): self.nested_clients() self.deprecated_options() @@ -1260,6 +1338,8 @@ class MetadataLint(Bcfg2.Server.Lint.ServerPlugin): "deprecated-clients-options": "warning"} def deprecated_options(self): + """ check for the location='floating' option, which has been + deprecated in favor of floating='true' """ clientdata = self.metadata.clients_xml.xdata for el in clientdata.xpath("//Client"): loc = el.get("location") @@ -1271,9 +1351,11 @@ class MetadataLint(Bcfg2.Server.Lint.ServerPlugin): self.LintError("deprecated-clients-options", "The location='%s' option is deprecated. " "Please use floating='%s' instead: %s" % - (loc, floating, self.RenderXML(el))) + (loc, floating, self.RenderXML(el))) def nested_clients(self): + """ check for a Client tag inside a Client tag, which doesn't + make any sense """ groupdata = self.metadata.groups_xml.xdata for el in groupdata.xpath("//Client//Client"): self.LintError("nested-client-tags", diff --git a/src/lib/Bcfg2/Server/Plugins/Probes.py b/src/lib/Bcfg2/Server/Plugins/Probes.py index 49e3b5e63..700c5e2e8 100644 --- a/src/lib/Bcfg2/Server/Plugins/Probes.py +++ b/src/lib/Bcfg2/Server/Plugins/Probes.py @@ -132,6 +132,12 @@ class ProbeSet(Bcfg2.Server.Plugin.EntrySet): encoding) fam.AddMonitor(path, self) + def HandleEvent(self, event): + """ handle events on everything but probed.xml """ + if (event.filename != self.path and + not event.filename.endswith("probed.xml")): + return self.handle_event(event) + def get_probe_data(self, metadata): """ Get an XML description of all probes for a client suitable for sending to that client. diff --git a/src/lib/Bcfg2/Server/__init__.py b/src/lib/Bcfg2/Server/__init__.py index f79b51dd3..3eb300a98 100644 --- a/src/lib/Bcfg2/Server/__init__.py +++ b/src/lib/Bcfg2/Server/__init__.py @@ -6,8 +6,8 @@ __all__ = ["Admin", "Core", "FileMonitor", "Plugin", "Plugins", "Hostbase", "Reports", "Snapshots", "XMLParser", "XI", "XI_NAMESPACE"] -XMLParser = lxml.etree.XMLParser(remove_blank_text=True) - XI = 'http://www.w3.org/2001/XInclude' XI_NAMESPACE = '{%s}' % XI +# pylint: disable=C0103 +XMLParser = lxml.etree.XMLParser(remove_blank_text=True) diff --git a/src/lib/Bcfg2/Server/models.py b/src/lib/Bcfg2/Server/models.py index bae6497a9..0328c6bea 100644 --- a/src/lib/Bcfg2/Server/models.py +++ b/src/lib/Bcfg2/Server/models.py @@ -1,15 +1,18 @@ +""" Django database models for all plugins """ + import sys import logging import Bcfg2.Options import Bcfg2.Server.Plugins from django.db import models -from Bcfg2.Compat import ConfigParser -logger = logging.getLogger('Bcfg2.Server.models') +LOGGER = logging.getLogger('Bcfg2.Server.models') MODELS = [] + def load_models(plugins=None, cfile='/etc/bcfg2.conf', quiet=True): + """ load models from plugins specified in the config """ global MODELS if plugins is None: @@ -19,9 +22,10 @@ def load_models(plugins=None, cfile='/etc/bcfg2.conf', quiet=True): plugin_opt = Bcfg2.Options.SERVER_PLUGINS plugin_opt.default = Bcfg2.Server.Plugins.__all__ - setup = Bcfg2.Options.OptionParser(dict(plugins=plugin_opt, - configfile=Bcfg2.Options.CFILE), - quiet=quiet) + setup = \ + Bcfg2.Options.OptionParser(dict(plugins=plugin_opt, + configfile=Bcfg2.Options.CFILE), + quiet=quiet) setup.parse([Bcfg2.Options.CFILE.cmd, cfile]) plugins = setup['plugins'] @@ -42,7 +46,7 @@ def load_models(plugins=None, cfile='/etc/bcfg2.conf', quiet=True): try: err = sys.exc_info()[1] mod = __import__(plugin) - except: + except: # pylint: disable=W0702 if plugins != Bcfg2.Server.Plugins.__all__: # only produce errors if the default plugin list # was not used -- i.e., if the config file was set @@ -50,7 +54,8 @@ def load_models(plugins=None, cfile='/etc/bcfg2.conf', quiet=True): # all plugins, IOW. the error from the first # attempt to import is probably more accurate than # the second attempt. - logger.error("Failed to load plugin %s: %s" % (plugin, err)) + LOGGER.error("Failed to load plugin %s: %s" % (plugin, + err)) continue for sym in dir(mod): obj = getattr(mod, sym) @@ -62,16 +67,16 @@ def load_models(plugins=None, cfile='/etc/bcfg2.conf', quiet=True): # and thus that this module will always work. load_models(quiet=True) -# Monitor our internal db version + class InternalDatabaseVersion(models.Model): - """Object that tell us to witch version is the database.""" + """ Object that tell us to which version the database is """ version = models.IntegerField() updated = models.DateTimeField(auto_now_add=True) def __str__(self): - return "version %d updated the %s" % (self.version, self.updated.isoformat()) + return "version %d updated the %s" % (self.version, + self.updated.isoformat()) - class Meta: + class Meta: # pylint: disable=C0111,W0232 app_label = "reports" get_latest_by = "version" - diff --git a/src/lib/Bcfg2/Statistics.py b/src/lib/Bcfg2/Statistics.py index bee90bbf4..2379dd1c8 100644 --- a/src/lib/Bcfg2/Statistics.py +++ b/src/lib/Bcfg2/Statistics.py @@ -1,4 +1,10 @@ +""" module for tracking execution time statistics from the bcfg2 +server core """ + + class Statistic(object): + """ a single named statistic, tracking minimum, maximum, and + average execution time, and number of invocations """ def __init__(self, name, initial_value): self.name = name self.min = float(initial_value) @@ -7,24 +13,29 @@ class Statistic(object): self.count = 1 def add_value(self, value): + """ add a value to the statistic """ self.min = min(self.min, value) self.max = max(self.max, value) self.ave = (((self.ave * (self.count - 1)) + value) / self.count) self.count += 1 def get_value(self): + """ get a tuple of all the stats tracked on this named item """ return (self.name, (self.min, self.max, self.ave, self.count)) class Statistics(object): + """ A collection of named statistics """ def __init__(self): self.data = dict() def add_value(self, name, value): + """ add a value to the named statistic """ if name not in self.data: self.data[name] = Statistic(name, value) else: self.data[name].add_value(value) def display(self): + """ return a dict of all statistics """ return dict([value.get_value() for value in list(self.data.values())]) diff --git a/src/lib/Bcfg2/settings.py b/src/lib/Bcfg2/settings.py index 7e18fde09..a904bead5 100644 --- a/src/lib/Bcfg2/settings.py +++ b/src/lib/Bcfg2/settings.py @@ -1,12 +1,14 @@ +""" Django settings for the Bcfg2 server """ + import os import sys import Bcfg2.Options try: import django - has_django = True -except: - has_django = False + HAS_DJANGO = True +except ImportError: + HAS_DJANGO = False DATABASES = dict() @@ -25,9 +27,11 @@ TEMPLATE_DEBUG = DEBUG MEDIA_URL = '/site_media' -# default config file is /etc/bcfg2-web.conf, UNLESS /etc/bcfg2.conf -# exists AND /etc/bcfg2-web.conf does not exist. + def _default_config(): + """ get the default config file. returns /etc/bcfg2-web.conf, + UNLESS /etc/bcfg2.conf exists AND /etc/bcfg2-web.conf does not + exist. """ optinfo = dict(cfile=Bcfg2.Options.CFILE, web_cfile=Bcfg2.Options.WEB_CFILE) setup = Bcfg2.Options.OptionParser(optinfo, quiet=True) @@ -40,16 +44,20 @@ def _default_config(): DEFAULT_CONFIG = _default_config() + def read_config(cfile=DEFAULT_CONFIG, repo=None, quiet=False): + """ read the config file and set django settings based on it """ + # pylint: disable=W0603 global DATABASE_ENGINE, DATABASE_NAME, DATABASE_USER, DATABASE_PASSWORD, \ DATABASE_HOST, DATABASE_PORT, DEBUG, TEMPLATE_DEBUG, TIME_ZONE, \ MEDIA_URL + # pylint: enable=W0603 if not os.path.exists(cfile) and os.path.exists(DEFAULT_CONFIG): - print("%s does not exist, using %s for database configuration" % + print("%s does not exist, using %s for database configuration" % (cfile, DEFAULT_CONFIG)) cfile = DEFAULT_CONFIG - + optinfo = Bcfg2.Options.DATABASE_COMMON_OPTIONS optinfo['repo'] = Bcfg2.Options.SERVER_REPOSITORY # when setting a different config file, it has to be set in either @@ -71,7 +79,7 @@ def read_config(cfile=DEFAULT_CONFIG, repo=None, quiet=False): HOST=setup['db_host'], PORT=setup['db_port']) - if has_django and django.VERSION[0] == 1 and django.VERSION[1] < 2: + if HAS_DJANGO and django.VERSION[0] == 1 and django.VERSION[1] < 2: DATABASE_ENGINE = setup['db_engine'] DATABASE_NAME = DATABASES['default']['NAME'] DATABASE_USER = DATABASES['default']['USER'] @@ -166,7 +174,7 @@ TEMPLATE_DIRS = ( ) # TODO - sanitize this -if has_django and django.VERSION[0] == 1 and django.VERSION[1] < 2: +if HAS_DJANGO and django.VERSION[0] == 1 and django.VERSION[1] < 2: TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.auth', 'django.core.context_processors.debug', @@ -182,4 +190,3 @@ else: 'django.core.context_processors.media', 'django.core.context_processors.request' ) - diff --git a/src/lib/Bcfg2/version.py b/src/lib/Bcfg2/version.py index cd622eafc..09b8e5153 100644 --- a/src/lib/Bcfg2/version.py +++ b/src/lib/Bcfg2/version.py @@ -1,8 +1,14 @@ +""" bcfg2 version declaration and handling """ + import re __version__ = "1.3.0pre1" + class Bcfg2VersionInfo(tuple): + """ object to make granular version operations (particularly + comparisons) easier """ + v_re = re.compile(r'(\d+)(\w+)(\d+)') def __new__(cls, vstr): @@ -17,30 +23,31 @@ class Bcfg2VersionInfo(tuple): return tuple.__new__(cls, [int(major), int(minor), int(micro), releaselevel, int(serial)]) - def __init__(self, vstr): + def __init__(self, vstr): # pylint: disable=W0613 tuple.__init__(self) self.major, self.minor, self.micro, self.releaselevel, self.serial = \ tuple(self) - + def __repr__(self): return "(major=%s, minor=%s, micro=%s, releaselevel=%s, serial=%s)" % \ tuple(self) - def _release_cmp(self, r1, r2): - if r1 == r2: + def _release_cmp(self, rel1, rel2): # pylint: disable=R0911 + """ compare two release numbers """ + if rel1 == rel2: return 0 - elif r1 == "final": + elif rel1 == "final": return -1 - elif r2 == "final": + elif rel2 == "final": return 1 - elif r1 == "rc": + elif rel1 == "rc": return -1 - elif r2 == "rc": + elif rel2 == "rc": return 1 # should never get to anything past this point - elif r1 == "pre": + elif rel1 == "pre": return -1 - elif r2 == "pre": + elif rel2 == "pre": return 1 else: # wtf? @@ -54,19 +61,12 @@ class Bcfg2VersionInfo(tuple): return True try: for i in range(3): - if self[i] > version[i]: - return True - elif self[i] < version[i]: - return False + if self[i] != version[i]: + return self[i] > version[i] rel = self._release_cmp(self[3], version[3]) - if rel < 0: - return True - elif rel > 0: - return False - if self[4] > version[4]: - return True - else: - return False + if rel != 0: + return rel < 0 + return self[4] > version[4] except TypeError: return self > Bcfg2VersionInfo(version) @@ -78,19 +78,12 @@ class Bcfg2VersionInfo(tuple): return False try: for i in range(3): - if self[i] < version[i]: - return True - elif self[i] > version[i]: - return False + if self[i] != version[i]: + return self[i] < version[i] rel = self._release_cmp(self[3], version[3]) - if rel > 0: - return True - elif rel < 0: - return False - if self[4] < version[4]: - return True - else: - return False + if rel != 0: + return rel > 0 + return self[4] < version[4] except TypeError: return self < Bcfg2VersionInfo(version) diff --git a/src/sbin/bcfg2-crypt b/src/sbin/bcfg2-crypt index bae4ad8ef..904f77611 100755 --- a/src/sbin/bcfg2-crypt +++ b/src/sbin/bcfg2-crypt @@ -9,59 +9,48 @@ import lxml.etree import Bcfg2.Logger import Bcfg2.Options from Bcfg2.Server import XMLParser -from Bcfg2.Compat import input +from Bcfg2.Compat import input # pylint: disable=W0622 try: import Bcfg2.Encryption except ImportError: - err = sys.exc_info()[1] - print("Could not import %s. Is M2Crypto installed?" % err) + print("Could not import %s. Is M2Crypto installed?" % sys.exc_info()[1]) raise SystemExit(1) -LOGGER = None - -def get_logger(verbose=0): - """ set up logging according to the verbose level given on the - command line """ - global LOGGER - if LOGGER is None: - LOGGER = logging.getLogger(sys.argv[0]) - stderr = logging.StreamHandler() - if verbose: - level = logging.DEBUG - else: - level = logging.WARNING - LOGGER.setLevel(level) - LOGGER.addHandler(stderr) - syslog = logging.handlers.SysLogHandler("/dev/log") - syslog.setFormatter(logging.Formatter("%(name)s: %(message)s")) - LOGGER.addHandler(syslog) - return LOGGER - class EncryptionChunkingError(Exception): + """ error raised when Encryptor cannot break a file up into chunks + to be encrypted, or cannot reassemble the chunks """ pass class Encryptor(object): + """ Generic encryptor for all files """ + def __init__(self, setup): self.setup = setup - self.logger = get_logger() self.passphrase = None self.pname = None + self.logger = logging.getLogger(self.__class__.__name__) def get_encrypted_filename(self, plaintext_filename): + """ get the name of the file encrypted data should be written to """ return plaintext_filename def get_plaintext_filename(self, encrypted_filename): + """ get the name of the file decrypted data should be written to """ return encrypted_filename def chunk(self, data): + """ generator to break the file up into smaller chunks that + will each be individually encrypted or decrypted """ yield data - def unchunk(self, data, original): + def unchunk(self, data, original): # pylint: disable=W0613 + """ given chunks of a file, reassemble then into the whole file """ return data[0] def set_passphrase(self): + """ set the passphrase for the current file """ if (not self.setup.cfp.has_section("encryption") or self.setup.cfp.options("encryption") == 0): self.logger.error("No passphrases available in %s" % @@ -98,6 +87,7 @@ class Encryptor(object): return False def encrypt(self, fname): + """ encrypt the given file, returning the encrypted data """ try: plaintext = open(fname).read() except IOError: @@ -124,12 +114,16 @@ class Encryptor(object): return False return self.unchunk(crypted, plaintext) + # pylint: disable=W0613 def _encrypt(self, plaintext, passphrase, name=None): + """ encrypt a single chunk of a file """ return Bcfg2.Encryption.ssl_encrypt( plaintext, passphrase, Bcfg2.Encryption.get_algorithm(self.setup)) + # pylint: enable=W0613 def decrypt(self, fname): + """ decrypt the given file, returning the plaintext data """ try: crypted = open(fname).read() except IOError: @@ -149,12 +143,12 @@ class Encryptor(object): except Bcfg2.Encryption.EVPError: self.logger.info("Could not decrypt %s with the " "specified passphrase" % fname) - return False + continue except: err = sys.exc_info()[1] self.logger.error("Error decrypting %s: %s" % (fname, err)) - return False + continue except TypeError: pchunk = None for pname in self.setup.cfp.options('encryption'): @@ -175,7 +169,7 @@ class Encryptor(object): self.logger.error("Could not decrypt %s with any " "passphrase in %s" % (fname, self.setup['configfile'])) - return False + continue except EncryptionChunkingError: err = sys.exc_info()[1] self.logger.error("Error getting encrypted data from %s: %s" % @@ -190,7 +184,14 @@ class Encryptor(object): (fname, err)) return False + def _decrypt(self, crypted, passphrase): + """ decrypt a single chunk """ + return Bcfg2.Encryption.ssl_decrypt( + crypted, passphrase, + Bcfg2.Encryption.get_algorithm(self.setup)) + def write_encrypted(self, fname, data=None): + """ write encrypted data to disk """ if data is None: data = self.decrypt(fname) new_fname = self.get_encrypted_filename(fname) @@ -210,6 +211,7 @@ class Encryptor(object): return False def write_decrypted(self, fname, data=None): + """ write decrypted data to disk """ if data is None: data = self.decrypt(fname) new_fname = self.get_plaintext_filename(fname) @@ -224,6 +226,7 @@ class Encryptor(object): return False def get_passphrase(self, chunk): + """ get the passphrase for a chunk of a file """ pname = self._get_passphrase(chunk) if not self.pname: if not pname: @@ -246,16 +249,14 @@ class Encryptor(object): (self.pname, pname)) return (passphrase, pname) - def _get_passphrase(self, chunk): + def _get_passphrase(self, chunk): # pylint: disable=W0613 + """ get the passphrase for a chunk of a file """ return None - def _decrypt(self, crypted, passphrase): - return Bcfg2.Encryption.ssl_decrypt( - crypted, passphrase, - Bcfg2.Encryption.get_algorithm(self.setup)) - class CfgEncryptor(Encryptor): + """ encryptor class for Cfg files """ + def get_encrypted_filename(self, plaintext_filename): return plaintext_filename + ".crypt" @@ -267,6 +268,8 @@ class CfgEncryptor(Encryptor): class PropertiesEncryptor(Encryptor): + """ encryptor class for Properties files """ + def _encrypt(self, plaintext, passphrase, name=None): # plaintext is an lxml.etree._Element if name is None: @@ -347,7 +350,7 @@ class PropertiesEncryptor(Encryptor): return crypted -def main(): +def main(): # pylint: disable=R0912,R0915 optinfo = dict(interactive=Bcfg2.Options.INTERACTIVE) optinfo.update(Bcfg2.Options.CRYPT_OPTIONS) optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS) @@ -360,35 +363,40 @@ def main(): print(setup.hm) raise SystemExit(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-crypt', **log_args) + logger = logging.getLogger('bcfg2-crypt') + if setup['decrypt']: if setup['encrypt']: - print("You cannot specify both --encrypt and --decrypt") + logger.error("You cannot specify both --encrypt and --decrypt") raise SystemExit(1) elif setup['remove']: - print("--remove cannot be used with --decrypt, ignoring") + logger.error("--remove cannot be used with --decrypt, ignoring") setup['remove'] = Bcfg2.Options.CRYPT_REMOVE.default elif setup['interactive']: - print("Cannot decrypt interactively") + logger.error("Cannot decrypt interactively") setup['interactive'] = False if setup['cfg']: if setup['properties']: - print("You cannot specify both --cfg and --properties") + logger.error("You cannot specify both --cfg and --properties") raise SystemExit(1) if setup['xpath']: - print("Specifying --xpath with --cfg is nonsensical, ignoring " - "--xpath") + logger.error("Specifying --xpath with --cfg is nonsensical, " + "ignoring --xpath") setup['xpath'] = Bcfg2.Options.CRYPT_XPATH.default if setup['interactive']: - print("You cannot use interactive mode with --cfg, ignoring -I") + logger.error("You cannot use interactive mode with --cfg, " + "ignoring -I") setup['interactive'] = False elif setup['properties']: if setup['remove']: - print("--remove cannot be used with --properties, ignoring") + logger.error("--remove cannot be used with --properties, ignoring") setup['remove'] = Bcfg2.Options.CRYPT_REMOVE.default - logger = get_logger(setup['verbose']) - props_crypt = PropertiesEncryptor(setup) cfg_crypt = CfgEncryptor(setup) @@ -456,7 +464,7 @@ def main(): if data is None: data = xform(fname) if not data: - print("Failed to %s %s, skipping" % (xform.__name__, fname)) + logger.error("Failed to %s %s, skipping" % (xform.__name__, fname)) continue if setup['crypt_stdout']: if len(setup['args']) > 1: diff --git a/src/sbin/bcfg2-lint b/src/sbin/bcfg2-lint index f1f91b7f4..0321d3045 100755 --- a/src/sbin/bcfg2-lint +++ b/src/sbin/bcfg2-lint @@ -9,108 +9,89 @@ import Bcfg2.Logger import Bcfg2.Options import Bcfg2.Server.Core import Bcfg2.Server.Lint -# Compatibility imports from Bcfg2.Compat import ConfigParser -logger = logging.getLogger('bcfg2-lint') +LOGGER = logging.getLogger('bcfg2-lint') -def run_serverless_plugins(plugins, config=None, setup=None, errorhandler=None): - logger.debug("Running serverless plugins") + +def run_serverless_plugins(plugins, setup=None, 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, - setup=setup, config=config, files=files) + setup=setup, files=files) + -def run_server_plugins(plugins, config=None, setup=None, errorhandler=None): +def run_server_plugins(plugins, setup=None, errorhandler=None, files=None): + """ run plugins that require a running server to run """ core = load_server(setup) - logger.debug("Running server plugins") + LOGGER.debug("Running server plugins") for plugin_name, plugin in list(plugins.items()): run_plugin(plugin, plugin_name, args=[core], errorhandler=errorhandler, - setup=setup, config=config, files=files) + setup=setup, files=files) + def run_plugin(plugin, plugin_name, setup=None, errorhandler=None, - args=None, config=None, files=None): - logger.debug(" Running %s" % plugin_name) + 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(config) + errorhandler = get_errorhandler(setup) - if config is not None and config.has_section(plugin_name): + if setup is not None and setup.cfp.has_section(plugin_name): arg = setup - for key, val in config.items(plugin_name): + for key, val in setup.cfp.items(plugin_name): arg[key] = val args.append(arg) else: args.append(setup) - - # older versions of python do not support mixing *-magic and - # non-*-magic (e.g., "plugin(*args, files=files)", so we do this - # all with *-magic - kwargs = dict(files=files, errorhandler=errorhandler) - - return plugin(*args, **kwargs).Run() - -def get_errorhandler(config): + + return plugin(*args, files=files, errorhandler=errorhandler).Run() + + +def get_errorhandler(setup): """ get a Bcfg2.Server.Lint.ErrorHandler object """ - if config.has_section("errors"): - conf = dict(config.items("errors")) + if setup.cfp.has_section("errors"): + conf = dict(setup.cfp.items("errors")) else: conf = None return Bcfg2.Server.Lint.ErrorHandler(config=conf) + def load_server(setup): """ load server """ core = Bcfg2.Server.Core.BaseCore(setup) core.fam.handle_events_in_interval(4) 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 p in parts[1:]: - mod = getattr(mod, p) + for part in parts[1:]: + mod = getattr(mod, part) return getattr(mod, obj_name) -if __name__ == '__main__': - optinfo = dict(config=Bcfg2.Options.LINT_CONFIG, - showerrors=Bcfg2.Options.LINT_SHOW_ERRORS, - stdin=Bcfg2.Options.LINT_FILES_ON_STDIN, - schema=Bcfg2.Options.SCHEMA_PATH, - plugins=Bcfg2.Options.SERVER_PLUGINS) - optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS) - optinfo.update(Bcfg2.Options.SERVER_COMMON_OPTIONS) - setup = Bcfg2.Options.OptionParser(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) - - config = ConfigParser.SafeConfigParser() - config.read(setup['configfile']) - config.read(setup['config']) - # get list of plugins to run +def load_plugins(setup): + """ get list of plugins to run """ if setup['args']: plugin_list = setup['args'] elif "bcfg2-repo-validate" in sys.argv[0]: plugin_list = 'Duplicates,RequiredAttrs,Validate'.split(',') else: try: - plugin_list = config.get('lint', 'plugins').split(',') + plugin_list = setup.cfp.get('lint', 'plugins').split(',') except (ConfigParser.NoOptionError, ConfigParser.NoSectionError): plugin_list = Bcfg2.Server.Lint.__all__ - if setup['stdin']: - files = [s.strip() for s in sys.stdin.readlines()] - else: - files = None - allplugins = dict() for plugin in plugin_list: try: @@ -121,12 +102,12 @@ if __name__ == '__main__': 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)) + 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)) + LOGGER.error("Failed to load plugin %s: %s" % (plugin, err)) serverplugins = dict() serverlessplugins = dict() @@ -136,21 +117,47 @@ if __name__ == '__main__': 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, + plugins=Bcfg2.Options.SERVER_PLUGINS) + optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS) + optinfo.update(Bcfg2.Options.SERVER_COMMON_OPTIONS) + setup = Bcfg2.Options.OptionParser(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) - errorhandler = get_errorhandler(config) + setup.cfp.read(setup['lint_config']) + + if setup['stdin']: + files = [s.strip() for s in sys.stdin.readlines()] + else: + files = None + + (serverlessplugins, serverplugins) = load_plugins(setup) + + errorhandler = get_errorhandler(setup) 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._handlers.items(): + for err, handler in errorhandler.errors.items(): print("%-35s %-35s" % (err, handler.__name__)) raise SystemExit(0) - run_serverless_plugins(serverlessplugins, - errorhandler=errorhandler, - config=config, setup=setup) + run_serverless_plugins(serverlessplugins, errorhandler=errorhandler, + setup=setup, files=files) if serverplugins: if errorhandler.errors: @@ -166,7 +173,7 @@ if __name__ == '__main__': "plugins") else: run_server_plugins(serverplugins, errorhandler=errorhandler, - config=config, setup=setup) + setup=setup, files=files) if errorhandler.errors or errorhandler.warnings or setup['verbose']: print("%d errors" % errorhandler.errors) @@ -176,3 +183,6 @@ if __name__ == '__main__': raise SystemExit(2) elif errorhandler.warnings: raise SystemExit(3) + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/sbin/bcfg2-test b/src/sbin/bcfg2-test index 8323eeb22..815d2740c 100755 --- a/src/sbin/bcfg2-test +++ b/src/sbin/bcfg2-test @@ -32,6 +32,8 @@ class ClientTest(TestCase): self.ignore = ignore def ignore_entry(self, tag, name): + """ return True if an error on a given entry should be ignored + """ if tag in self.ignore: if name in self.ignore[tag]: return True @@ -43,6 +45,7 @@ class ClientTest(TestCase): return False def runTest(self): + """ run this individual test """ config = self.bcfg2_core.BuildConfiguration(self.client) failures = [] @@ -86,6 +89,7 @@ def main(): ignore[tag] = [name] def run_tests(): + """ Run the test suite """ core.fam.handle_events_in_interval(0.1) if setup['args']: |