diff options
Diffstat (limited to 'src/lib/Bcfg2/Client/Tools/__init__.py')
-rw-r--r-- | src/lib/Bcfg2/Client/Tools/__init__.py | 364 |
1 files changed, 364 insertions, 0 deletions
diff --git a/src/lib/Bcfg2/Client/Tools/__init__.py b/src/lib/Bcfg2/Client/Tools/__init__.py new file mode 100644 index 000000000..d423b6380 --- /dev/null +++ b/src/lib/Bcfg2/Client/Tools/__init__.py @@ -0,0 +1,364 @@ +"""This contains all Bcfg2 Tool modules""" +import os +import stat +import sys +from subprocess import Popen, PIPE +import time + +import Bcfg2.Client.XML +from Bcfg2.Bcfg2Py3k import input + +__all__ = [tool.split('.')[0] \ + for tool in os.listdir(os.path.dirname(__file__)) \ + if tool.endswith(".py") and tool != "__init__.py"] + +drivers = [item for item in __all__ if item not in ['rpmtools']] +default = [item for item in drivers if item not in ['RPM', 'Yum']] + + +class toolInstantiationError(Exception): + """This error is called if the toolset cannot be instantiated.""" + pass + + +class executor: + """This class runs stuff for us""" + + def __init__(self, logger): + self.logger = logger + + def run(self, command): + """Run a command in a pipe dealing with stdout buffer overloads.""" + p = Popen(command, shell=True, bufsize=16384, + stdin=PIPE, stdout=PIPE, close_fds=True) + output = p.communicate()[0] + for line in output.splitlines(): + self.logger.debug('< %s' % line) + return (p.returncode, output.splitlines()) + + +class Tool: + """ + All tools subclass this. It defines all interfaces that need to be defined. + """ + name = 'Tool' + __execs__ = [] + __handles__ = [] + __req__ = {} + __important__ = [] + + def __init__(self, logger, setup, config): + self.__important__ = [entry.get('name') \ + for struct in config for entry in struct \ + if entry.tag == 'Path' and \ + entry.get('important') in ['true', 'True']] + self.setup = setup + self.logger = logger + if not hasattr(self, '__ireq__'): + self.__ireq__ = self.__req__ + self.config = config + self.cmd = executor(logger) + self.modified = [] + self.extra = [] + self.handled = [entry for struct in self.config for entry in struct \ + if self.handlesEntry(entry)] + for filename in self.__execs__: + try: + mode = stat.S_IMODE(os.stat(filename)[stat.ST_MODE]) + if mode & stat.S_IEXEC != stat.S_IEXEC: + self.logger.debug("%s: %s not executable" % \ + (self.name, filename)) + raise toolInstantiationError + except OSError: + raise toolInstantiationError + except: + self.logger.debug("%s failed" % filename, exc_info=1) + raise toolInstantiationError + + def BundleUpdated(self, _, states): + """This callback is used when bundle updates occur.""" + return + + def BundleNotUpdated(self, _, states): + """This callback is used when a bundle is not updated.""" + return + + def Inventory(self, states, structures=[]): + """Dispatch verify calls to underlying methods.""" + if not structures: + structures = self.config.getchildren() + mods = self.buildModlist() + for (struct, entry) in [(struct, entry) for struct in structures \ + for entry in struct.getchildren() \ + if self.canVerify(entry)]: + try: + func = getattr(self, "Verify%s" % (entry.tag)) + states[entry] = func(entry, mods) + except: + self.logger.error( + "Unexpected failure of verification method for entry type %s" \ + % (entry.tag), exc_info=1) + self.extra = self.FindExtra() + + def Install(self, entries, states): + """Install all entries in sublist.""" + for entry in entries: + try: + func = getattr(self, "Install%s" % (entry.tag)) + states[entry] = func(entry) + if states[entry]: + self.modified.append(entry) + except: + self.logger.error("Unexpected failure of install method for entry type %s" \ + % (entry.tag), exc_info=1) + + def Remove(self, entries): + """Remove specified extra entries""" + pass + + def getSupportedEntries(self): + """Return a list of supported entries.""" + return [entry for struct in \ + self.config.getchildren() for entry in \ + struct.getchildren() \ + if self.handlesEntry(entry)] + + def handlesEntry(self, entry): + """Return if entry is handled by this tool.""" + return (entry.tag, entry.get('type')) in self.__handles__ + + def buildModlist(self): + '''Build a list of potentially modified POSIX paths for this entry''' + return [entry.get('name') for struct in self.config.getchildren() \ + for entry in struct.getchildren() \ + if entry.tag in ['Ignore', 'Path']] + + def gatherCurrentData(self, entry): + """Default implementation of the information gathering routines.""" + pass + + def canVerify(self, entry): + """Test if entry has enough information to be verified.""" + if not self.handlesEntry(entry): + return False + + if 'failure' in entry.attrib: + self.logger.error("Entry %s:%s reports bind failure: %s" % \ + (entry.tag, + entry.get('name'), + entry.get('failure'))) + return False + + missing = [attr for attr in self.__req__[entry.tag] \ + if attr not in entry.attrib] + if missing: + self.logger.error("Incomplete information for entry %s:%s; cannot verify" \ + % (entry.tag, entry.get('name'))) + self.logger.error("\t... due to absence of %s attribute(s)" % \ + (":".join(missing))) + try: + self.gatherCurrentData(entry) + except: + self.logger.error("Unexpected error in gatherCurrentData", + exc_info=1) + return False + return True + + def FindExtra(self): + """Return a list of extra entries.""" + return [] + + def canInstall(self, entry): + """Test if entry has enough information to be installed.""" + if not self.handlesEntry(entry): + return False + + if 'failure' in entry.attrib: + self.logger.error("Cannot install entry %s:%s with bind failure" % \ + (entry.tag, entry.get('name'))) + return False + + missing = [attr for attr in self.__ireq__[entry.tag] \ + if attr not in entry.attrib or not entry.attrib[attr]] + if missing: + self.logger.error("Incomplete information for entry %s:%s; cannot install" \ + % (entry.tag, entry.get('name'))) + self.logger.error("\t... due to absence of %s attribute" % \ + (":".join(missing))) + return False + return True + + +class PkgTool(Tool): + """ + PkgTool provides a one-pass install with + fallback for use with packaging systems + """ + pkgtool = ('echo %s', ('%s', ['name'])) + pkgtype = 'echo' + name = 'PkgTool' + + def __init__(self, logger, setup, config): + Tool.__init__(self, logger, setup, config) + self.installed = {} + self.Remove = self.RemovePackages + self.FindExtra = self.FindExtraPackages + self.RefreshPackages() + + def VerifyPackage(self, dummy, _): + """Dummy verification method""" + return False + + def Install(self, packages, states): + """ + Run a one-pass install, followed by + single pkg installs in case of failure. + """ + self.logger.info("Trying single pass package install for pkgtype %s" % \ + self.pkgtype) + + data = [tuple([pkg.get(field) for field in self.pkgtool[1][1]]) + for pkg in packages] + pkgargs = " ".join([self.pkgtool[1][0] % datum for datum in data]) + + self.logger.debug("Installing packages: :%s:" % pkgargs) + self.logger.debug("Running command ::%s::" % (self.pkgtool[0] % pkgargs)) + + cmdrc = self.cmd.run(self.pkgtool[0] % pkgargs)[0] + if cmdrc == 0: + self.logger.info("Single Pass Succeded") + # set all package states to true and flush workqueues + pkgnames = [pkg.get('name') for pkg in packages] + for entry in [entry for entry in list(states.keys()) + if entry.tag == 'Package' + and entry.get('type') == self.pkgtype + and entry.get('name') in pkgnames]: + self.logger.debug('Setting state to true for pkg %s' % \ + (entry.get('name'))) + states[entry] = True + self.RefreshPackages() + else: + self.logger.error("Single Pass Failed") + # do single pass installs + self.RefreshPackages() + for pkg in packages: + # handle state tracking updates + if self.VerifyPackage(pkg, []): + self.logger.info("Forcing state to true for pkg %s" % \ + (pkg.get('name'))) + states[pkg] = True + else: + self.logger.info("Installing pkg %s version %s" % + (pkg.get('name'), pkg.get('version'))) + cmdrc = self.cmd.run(self.pkgtool[0] % + (self.pkgtool[1][0] % + tuple([pkg.get(field) for field in self.pkgtool[1][1]]))) + if cmdrc[0] == 0: + states[pkg] = True + else: + self.logger.error("Failed to install package %s" % \ + (pkg.get('name'))) + self.RefreshPackages() + for entry in [ent for ent in packages if states[ent]]: + self.modified.append(entry) + + def RefreshPackages(self): + """Dummy state refresh method.""" + pass + + def RemovePackages(self, packages): + """Dummy implementation of package removal method.""" + pass + + def FindExtraPackages(self): + """Find extra packages.""" + packages = [entry.get('name') for entry in self.getSupportedEntries()] + extras = [data for data in list(self.installed.items()) \ + if data[0] not in packages] + return [Bcfg2.Client.XML.Element('Package', name=name, \ + type=self.pkgtype, version=version) \ + for (name, version) in extras] + + +class SvcTool(Tool): + """This class defines basic Service behavior""" + name = 'SvcTool' + + def __init__(self, logger, setup, config): + Tool.__init__(self, logger, setup, config) + self.restarted = [] + + def get_svc_command(self, service, action): + """Return the basename of the command used to start/stop services.""" + return '/etc/init.d/%s %s' % (service.get('name'), action) + + def start_service(self, service): + self.logger.debug('Starting service %s' % service.get('name')) + return self.cmd.run(self.get_svc_command(service, 'start'))[0] + + def stop_service(self, service): + self.logger.debug('Stopping service %s' % service.get('name')) + return self.cmd.run(self.get_svc_command(service, 'stop'))[0] + + def restart_service(self, service): + self.logger.debug('Restarting service %s' % service.get('name')) + restart_target = service.get('target', 'restart') + return self.cmd.run(self.get_svc_command(service, restart_target))[0] + + def check_service(self, service): + return self.cmd.run(self.get_svc_command(service, 'status'))[0] == 0 + + def Remove(self, services): + """ Dummy implementation of service removal method """ + if self.setup['servicemode'] != 'disabled': + for entry in services: + entry.set("status", "off") + self.InstallService(entry) + + def BundleUpdated(self, bundle, states): + """The Bundle has been updated.""" + if self.setup['servicemode'] == 'disabled': + return + + for entry in [ent for ent in bundle if self.handlesEntry(ent)]: + restart = entry.get("restart", "true") + if (restart.lower() == "false" or + (restart.lower == "interactive" and + not self.setup['interactive'])): + continue + + rc = None + if entry.get('status') == 'on': + if self.setup['servicemode'] == 'build': + rc = self.stop_service(entry) + elif entry.get('name') not in self.restarted: + if self.setup['interactive']: + prompt = ('Restart service %s?: (y/N): ' % + entry.get('name')) + ans = input(prompt) + if ans not in ['y', 'Y']: + continue + rc = self.restart_service(entry) + if not rc: + self.restarted.append(entry.get('name')) + else: + rc = self.stop_service(entry) + if rc: + self.logger.error("Failed to manipulate service %s" % + (entry.get('name'))) + + def Install(self, entries, states): + """Install all entries in sublist.""" + for entry in entries: + if entry.get('install', 'true').lower() == 'false': + self.logger.info("Service %s installation is false. Skipping " + "installation." % (entry.get('name'))) + continue + try: + func = getattr(self, "Install%s" % (entry.tag)) + states[entry] = func(entry) + if states[entry]: + self.modified.append(entry) + except: + self.logger.error("Unexpected failure of install method for entry type %s" + % (entry.tag), exc_info=1) |