summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Client/Frame.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Bcfg2/Client/Frame.py')
-rw-r--r--src/lib/Bcfg2/Client/Frame.py454
1 files changed, 454 insertions, 0 deletions
diff --git a/src/lib/Bcfg2/Client/Frame.py b/src/lib/Bcfg2/Client/Frame.py
new file mode 100644
index 000000000..baac70f52
--- /dev/null
+++ b/src/lib/Bcfg2/Client/Frame.py
@@ -0,0 +1,454 @@
+"""
+Frame is the Client Framework that verifies and
+installs entries, and generates statistics.
+"""
+
+import logging
+import sys
+import time
+import Bcfg2.Client.Tools
+
+
+def cmpent(ent1, ent2):
+ """Sort entries."""
+ if ent1.tag != ent2.tag:
+ return cmp(ent1.tag, ent2.tag)
+ else:
+ return cmp(ent1.get('name'), ent2.get('name'))
+
+
+def promptFilter(prompt, entries):
+ """Filter a supplied list based on user input."""
+ ret = []
+ entries.sort(cmpent)
+ for entry in entries[:]:
+ if 'qtext' in entry.attrib:
+ iprompt = entry.get('qtext')
+ else:
+ iprompt = prompt % (entry.tag, entry.get('name'))
+ try:
+ # py3k compatibility
+ try:
+ ans = raw_input(iprompt.encode(sys.stdout.encoding, 'replace'))
+ except NameError:
+ ans = input(iprompt)
+ if ans in ['y', 'Y']:
+ ret.append(entry)
+ except EOFError:
+ # python 2.4.3 on CentOS doesn't like ^C for some reason
+ break
+ except:
+ print("Error while reading input")
+ continue
+ return ret
+
+
+def matches_entry(entryspec, entry):
+ # both are (tag, name)
+ 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
+
+
+def matches_white_list(entry, whitelist):
+ return True in [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]
+
+
+class Frame:
+ """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
+ self.dryrun = dryrun
+ self.times['initialization'] = time.time()
+ self.setup = setup
+ self.tools = []
+ self.states = {}
+ self.whitelist = []
+ self.blacklist = []
+ self.removal = []
+ self.logger = logging.getLogger("Bcfg2.Client.Frame")
+ for driver in drivers[:]:
+ if driver not in Bcfg2.Client.Tools.drivers and \
+ isinstance(driver, str):
+ self.logger.error("Tool driver %s is not available" % driver)
+ drivers.remove(driver)
+
+ tclass = {}
+ for tool in drivers:
+ if not isinstance(tool, str):
+ tclass[time.time()] = tool
+ tool_class = "Bcfg2.Client.Tools.%s" % tool
+ try:
+ tclass[tool] = getattr(__import__(tool_class, globals(),
+ locals(), ['*']),
+ tool)
+ except ImportError:
+ continue
+ except:
+ self.logger.error("Tool %s unexpectedly failed to load" % tool,
+ exc_info=1)
+
+ for tool in list(tclass.values()):
+ try:
+ self.tools.append(tool(self.logger, setup, config))
+ except Bcfg2.Client.Tools.toolInstantiationError:
+ continue
+ except:
+ self.logger.error("Failed to instantiate tool %s" % \
+ (tool), exc_info=1)
+
+ for tool in self.tools[:]:
+ for conflict in getattr(tool, 'conflicts', []):
+ [self.tools.remove(item) for item in self.tools \
+ if item.name == conflict]
+
+ self.logger.info("Loaded tool drivers:")
+ self.logger.info([tool.name for tool in self.tools])
+
+ # find entries not handled by any tools
+ problems = [entry for struct in config for \
+ entry in struct if entry not in self.handled]
+
+ if problems:
+ self.logger.error("The following entries are not handled by any tool:")
+ self.logger.error(["%s:%s:%s" % (entry.tag, entry.get('type'), \
+ entry.get('name')) for entry in problems])
+ self.logger.error("")
+ entries = [(entry.tag, entry.get('name'))
+ for struct in config for entry in struct]
+ pkgs = [(entry.get('name'), entry.get('origin'))
+ for struct in config for entry in struct if entry.tag == 'Package']
+ multi = []
+ for entry in entries[:]:
+ if entries.count(entry) > 1:
+ multi.append(entry)
+ entries.remove(entry)
+ if multi:
+ self.logger.debug("The following entries are included multiple times:")
+ self.logger.debug(["%s:%s" % entry for entry in multi])
+ self.logger.debug("")
+ 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([pkg[0] for pkg in pkgs if pkg[1] == 'Packages'])
+
+ def __getattr__(self, name):
+ if name in ['extra', 'handled', 'modified', '__important__']:
+ ret = []
+ for tool in self.tools:
+ ret += getattr(tool, name)
+ return ret
+ elif name in self.__dict__:
+ return self.__dict__[name]
+ raise AttributeError(name)
+
+ def InstallImportant(self):
+ """Install important entries
+
+ We also process the decision mode stuff here because we want to prevent
+ 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
+ self.whitelist = [entry for entry in self.states \
+ if not self.states[entry]]
+ if not self.setup['file']:
+ if self.setup['decision'] == 'whitelist':
+ dwl = self.setup['decision_list']
+ 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.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'])]
+ 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]
+
+ # take care of important entries first
+ if not self.dryrun and not self.setup['bundle']:
+ for cfile in [cfl for cfl in self.config.findall(".//Path") \
+ if cfl.get('name') in self.__important__ and \
+ cfl.get('type') == 'file']:
+ if cfile not in self.whitelist:
+ continue
+ tl = [t for t in self.tools if t.handlesEntry(cfile) \
+ and t.canVerify(cfile)]
+ if tl:
+ if self.setup['interactive'] and not \
+ promptFilter("Install %s: %s? (y/N):", [cfile]):
+ self.whitelist.remove(cfile)
+ continue
+ try:
+ self.states[cfile] = tl[0].InstallPath(cfile)
+ if self.states[cfile]:
+ tl[0].modified.append(cfile)
+ except:
+ self.logger.error("Unexpected tool failure",
+ exc_info=1)
+ cfile.set('qtext', '')
+ if tl[0].VerifyPath(cfile, []):
+ self.whitelist.remove(cfile)
+
+ def Inventory(self):
+ """
+ Verify all entries,
+ find extra entries,
+ and build up workqueues
+
+ """
+ # initialize all states
+ for struct in self.config.getchildren():
+ for entry in struct.getchildren():
+ self.states[entry] = False
+ for tool in self.tools:
+ try:
+ tool.Inventory(self.states)
+ except:
+ self.logger.error("%s.Inventory() call failed:" % tool.name, exc_info=1)
+
+ def Decide(self):
+ """Set self.whitelist based on user interaction."""
+ prompt = "Install %s: %s? (y/N): "
+ rprompt = "Remove %s: %s? (y/N): "
+ if self.setup['remove']:
+ if self.setup['remove'] == 'all':
+ self.removal = self.extra
+ elif self.setup['remove'] in ['services', 'Services']:
+ self.removal = [entry for entry in self.extra
+ if entry.tag == 'Service']
+ elif self.setup['remove'] in ['packages', 'Packages']:
+ self.removal = [entry for entry in self.extra
+ if entry.tag == 'Package']
+
+ candidates = [entry for entry in self.states
+ if not self.states[entry]]
+
+ if self.dryrun:
+ if self.whitelist:
+ 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(["%s:%s" % (entry.tag, entry.get('name'))
+ for entry in self.removal])
+ self.removal = []
+ return
+ # Here is where most of the work goes
+ # first perform bundle filtering
+ if self.setup['bundle']:
+ all_bundle_names = [b.get('name') for b in
+ self.config.findall('./Bundle')]
+ # warn if non-existent bundle given
+ for bundle in self.setup['bundle']:
+ if bundle not in all_bundle_names:
+ self.logger.info("Warning: Bundle %s not found" % bundle)
+ bundles = [b for b in self.config.findall('./Bundle')
+ if b.get('name') in self.setup['bundle']]
+ self.whitelist = [e for e in self.whitelist
+ if True in [e in b for b in bundles]]
+ elif self.setup['indep']:
+ bundles = [nb for nb in self.config.getchildren()
+ if nb.tag != 'Bundle']
+ else:
+ bundles = self.config.getchildren()
+
+ # first process prereq actions
+ for bundle in bundles[:]:
+ if bundle.tag != 'Bundle':
+ continue
+ 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'))]
+ # now we process all "always actions"
+ if self.setup['interactive']:
+ promptFilter(prompt, actions)
+ self.DispatchInstallCalls(actions)
+
+ # need to test to fail entries in whitelist
+ if False in [self.states[a] for a in actions]:
+ # then display bundles forced off with entries
+ self.logger.info("Bundle %s failed prerequisite action" %
+ (bundle.get('name')))
+ bundles.remove(bundle)
+ b_to_remv = [ent for ent in self.whitelist if ent in bundle]
+ if b_to_remv:
+ self.logger.info("Not installing entries from Bundle %s" %
+ (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]
+
+ if self.setup['interactive']:
+ self.whitelist = promptFilter(prompt, self.whitelist)
+ self.removal = promptFilter(rprompt, self.removal)
+
+ for entry in candidates:
+ if entry not in self.whitelist:
+ self.blacklist.append(entry)
+
+ def DispatchInstallCalls(self, entries):
+ """Dispatch install calls to underlying tools."""
+ for tool in self.tools:
+ handled = [entry for entry in entries if tool.canInstall(entry)]
+ if not handled:
+ continue
+ try:
+ tool.Install(handled, self.states)
+ except:
+ self.logger.error("%s.Install() call failed:" % tool.name, exc_info=1)
+
+ def Install(self):
+ """Install all entries."""
+ self.DispatchInstallCalls(self.whitelist)
+ mods = self.modified
+ mbundles = [struct for struct in self.config.findall('Bundle') if \
+ [mod for mod in mods if mod in struct]]
+
+ if self.modified:
+ # Handle Bundle interdeps
+ if mbundles:
+ self.logger.info("The Following Bundles have been modified:")
+ self.logger.info([mbun.get('name') for mbun in mbundles])
+ self.logger.info("")
+ tbm = [(t, b) for t in self.tools for b in mbundles]
+ for tool, bundle in tbm:
+ try:
+ tool.Inventory(self.states, [bundle])
+ except:
+ 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 clobbered:
+ self.logger.debug("Found clobbered entries:")
+ self.logger.debug(["%s:%s" % (entry.tag, entry.get('name')) \
+ for entry in clobbered])
+ if not self.setup['interactive']:
+ self.DispatchInstallCalls(clobbered)
+
+ for bundle in self.config.findall('.//Bundle'):
+ if self.setup['bundle'] and \
+ bundle.get('name') not in self.setup['bundle']:
+ # prune out unspecified bundles when running with -b
+ continue
+ for tool in self.tools:
+ try:
+ if bundle in mbundles:
+ tool.BundleUpdated(bundle, self.states)
+ else:
+ tool.BundleNotUpdated(bundle, self.states)
+ except:
+ 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)]
+ if extras:
+ try:
+ tool.Remove(extras)
+ except:
+ self.logger.error("%s.Remove() failed" % tool.name, exc_info=1)
+
+ def CondDisplayState(self, phase):
+ """Conditionally print tracing information."""
+ self.logger.info('\nPhase: %s' % phase)
+ self.logger.info('Correct entries:\t%d' % list(self.states.values()).count(True))
+ self.logger.info('Incorrect entries:\t%d' % list(self.states.values()).count(False))
+ if phase == 'final' and list(self.states.values()).count(False):
+ self.logger.info(["%s:%s" % (entry.tag, entry.get('name')) for \
+ entry in self.states if not self.states[entry]])
+ self.logger.info('Total managed entries:\t%d' % len(list(self.states.values())))
+ self.logger.info('Unmanaged entries:\t%d' % len(self.extra))
+ if phase == 'final' and self.setup['extra']:
+ self.logger.info(["%s:%s" % (entry.tag, entry.get('name')) \
+ for entry in self.extra])
+
+ self.logger.info("")
+
+ if ((list(self.states.values()).count(False) == 0) and not self.extra):
+ self.logger.info('All entries correct.')
+
+ def ReInventory(self):
+ """Recheck everything."""
+ if not self.dryrun and self.setup['kevlar']:
+ self.logger.info("Rechecking system inventory")
+ self.Inventory()
+
+ def Execute(self):
+ """Run all methods."""
+ self.Inventory()
+ self.times['inventory'] = time.time()
+ self.CondDisplayState('initial')
+ self.InstallImportant()
+ self.Decide()
+ self.Install()
+ self.times['install'] = time.time()
+ self.Remove()
+ self.times['remove'] = time.time()
+ if self.modified:
+ self.ReInventory()
+ self.times['reinventory'] = time.time()
+ self.times['finished'] = time.time()
+ self.CondDisplayState('final')
+
+ 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)),
+ client_version=__revision__,
+ version='2.0',
+ revision=self.config.get('revision', '-1'))
+ good = len([key for key, val in list(self.states.items()) if val])
+ 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:
+ stats.set('state', 'dirty')
+
+ # List bad elements of the configuration
+ for (data, ename) in [(self.modified, 'Modified'), (self.extra, "Extra"), \
+ ([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', '')
+ container.append(item)
+ item.text = None
+
+ timeinfo = Bcfg2.Client.XML.Element("OpStamps")
+ feedback.append(stats)
+ for (event, timestamp) in list(self.times.items()):
+ timeinfo.set(event, str(timestamp))
+ stats.append(timeinfo)
+ return feedback