From eac71fc1109f2edc6b71e62a6cff38d762bebe63 Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Tue, 25 Sep 2012 11:49:15 -0400 Subject: expanded pylint coverage --- src/lib/Bcfg2/Cache.py | 4 +- src/lib/Bcfg2/Client/Frame.py | 171 ++++++----- src/lib/Bcfg2/Client/XML.py | 17 +- src/lib/Bcfg2/Compat.py | 48 ++-- src/lib/Bcfg2/Encryption.py | 3 + src/lib/Bcfg2/Logger.py | 40 +-- src/lib/Bcfg2/Options.py | 102 +++++-- src/lib/Bcfg2/Server/FileMonitor/Fam.py | 31 +- src/lib/Bcfg2/Server/FileMonitor/Gamin.py | 4 +- src/lib/Bcfg2/Server/FileMonitor/Inotify.py | 39 +-- src/lib/Bcfg2/Server/FileMonitor/Pseudo.py | 11 +- src/lib/Bcfg2/Server/FileMonitor/__init__.py | 45 +-- src/lib/Bcfg2/Server/Lint/__init__.py | 26 +- src/lib/Bcfg2/Server/Plugins/Metadata.py | 320 +++++++++++++-------- src/lib/Bcfg2/Server/Plugins/Probes.py | 6 + src/lib/Bcfg2/Server/__init__.py | 4 +- src/lib/Bcfg2/Server/models.py | 29 +- src/lib/Bcfg2/Statistics.py | 11 + src/lib/Bcfg2/settings.py | 27 +- src/lib/Bcfg2/version.py | 61 ++-- src/sbin/bcfg2-crypt | 100 ++++--- src/sbin/bcfg2-lint | 134 +++++---- src/sbin/bcfg2-test | 4 + .../Testlib/TestServer/TestPlugins/TestMetadata.py | 14 +- .../Testlib/TestServer/TestPlugins/TestProbes.py | 38 ++- testsuite/Testsrc/test_code_checks.py | 85 +++--- testsuite/install.sh | 3 +- testsuite/pylintrc.conf | 16 +- 28 files changed, 825 insertions(+), 568 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 (, ) 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 (, ) 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