summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Client/Tools/__init__.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Bcfg2/Client/Tools/__init__.py')
-rw-r--r--src/lib/Bcfg2/Client/Tools/__init__.py353
1 files changed, 353 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..c6cb6e239
--- /dev/null
+++ b/src/lib/Bcfg2/Client/Tools/__init__.py
@@ -0,0 +1,353 @@
+"""This contains all Bcfg2 Tool modules"""
+import os
+import stat
+import sys
+from subprocess import Popen, PIPE
+import time
+
+import Bcfg2.Client.XML
+
+__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):
+ # not supported for this driver
+ return 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)]:
+ mode = entry.get('mode', 'default')
+ if (mode == 'manual' or
+ (mode == 'interactive_only' and
+ not self.setup['interactive'])):
+ continue
+ # need to handle servicemode = (build|default)
+ # need to handle mode = (default|supervised)
+ 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'))
+ # py3k compatibility
+ try:
+ ans = raw_input(prompt)
+ except NameError:
+ 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')))