#!/usr/bin/env python '''Bcfg2 Client''' __revision__ = '$Revision$' import logging, os, signal, tempfile, time, xmlrpclib import Bcfg2.Options, Bcfg2.Client.XML try: import Bcfg2.Client.Proxy, Bcfg2.Logging except KeyError: print "Could not read options from configuration file" raise SystemExit, 1 def cb_sigint_handler(signum, frame): '''Exit upon CTRL-C''' os._exit(1) class Client: ''' The main bcfg2 client class ''' def __init__(self): self.toolset = None self.config = None optinfo = { # 'optname': (('-a', argdesc, optdesc), # env, cfpath, default, boolean)), 'verbose':(('-v', False,"enable verbose output"), False, False, False, True), 'quick':(('-q', False, "disable some checksum verification"), False, False, False, True), 'debug':(('-d', False, "enable debugging output"), False, False, False, True), 'dryrun':(('-n', False, "do not actually change the system"), False, False, False, True), 'build': (('-B', False, "run in build mode"), False, False, False, True), 'paranoid':(('-P', False, "make automatic backups of config files"), False, False, False, True), 'bundle':(('-b', '', "only configure the given bundle"), False, False, False, False), 'file': (('-f', "", "configure from a file rather than querying the server"), False, False, False, False), 'cache': (('-c', "", "store the configuration in a file"), False, False, False, False), 'profile': (('-p', '', "assert the given profile for the host"), False, False, False, False), 'remove': (('-r', '(packages|services|all)', "force removal of additional configuration items"), False, False, False, False), 'help': (('-h', False, "print this help message"), False, False, False, True), 'setup': (('-C', '', "use given config file (default /etc/bcfg2.conf)"), False, False, '/etc/bcfg2.conf', False), 'server': (('-S', '', 'the server hostname to connect to'), False, ('components', 'bcfg2'), 'https://localhost:6789', False), 'user': (('-u', '', 'the user to provide for authentication'), False, ('communication', 'user'), 'root', False), 'password': (('-x', '', 'the password to provide for authentication'), False, ('communication', 'password'), 'password', False), 'retries': (('-R', '', 'the number of times to retry network communication'), False, ('communication', 'retries'), '3', False), 'kevlar': (('-k', False, "run in kevlar (bulletproof) mode"), False, False, False, True), } self.setup = Bcfg2.Options.OptionParser('bcfg2', optinfo).parse() level = 30 if self.setup['verbose']: level = 20 if self.setup['debug']: level = 0 Bcfg2.Logging.setup_logging('bcfg2', to_syslog=False, level=level) self.logger = logging.getLogger('bcfg2') self.logger.debug(self.setup) if self.setup['remove'] not in [False, 'all', 'services', 'packages']: self.logger.error("Got unknown argument %s for -r" % (self.setup['remove'])) if (self.setup["file"] != False) and (self.setup["cache"] != False): print "cannot use -f and -c together" raise SystemExit, 1 if self.setup['remove'] and self.setup['dryrun']: print "cannot use -n and -r together" raise SystemExit, 1 def load_toolset(self, toolsetName): '''Import client toolset modules''' toolset_packages = { 'debian': "Bcfg2.Client.Debian", 'rh': "Bcfg2.Client.Redhat", 'solaris': "Bcfg2.Client.Solaris" } if toolset_packages.has_key(toolsetName): toolset_class = toolset_packages[toolsetName] else: toolset_class = toolsetName try: mod = __import__(toolset_class, globals(), locals(), ['*']) except ImportError: self.fatal_error("Failed to load server-specified toolset: %s" % (toolsetName)) except: self.logger.error("Failed to import toolset %s" % (toolsetName), exc_info=1) self.fatal_error("Cannot continue") try: self.toolset = mod.ToolsetImpl(self.config, self.setup) self.logger.debug("Selected %s toolset..." % (toolsetName)) except: self.logger.error("Failed to instantiate toolset: %s" % (toolsetName), exc_info=1) raise SystemExit, 1 def run_probe(self, probe): '''Execute probe''' name = probe.get('name') ret = Bcfg2.Client.XML.Element("probe-data", name=name, source=probe.get('source')) try: script = open(tempfile.mktemp(), 'w+') try: script.write("#!%s\n" % (probe.attrib.get('interpreter', '/bin/sh'))) script.write(probe.text) script.close() os.chmod(script.name, 0755) ret.text = os.popen(script.name).read().strip() finally: os.unlink(script.name) except: self.logger.error("Failed to execute probe: %s" % (name), exc_info=1) raise SystemExit, 1 return ret def fatal_error(self, message): '''Signal a fatal error''' self.logger.error("Fatal error: %s" % (message)) raise SystemExit, 1 def run(self): ''' Perform client execution phase ''' times = {} # begin configuration times['start'] = time.time() if self.setup['file']: # read config from file try: self.logger.debug("reading cached configuration from %s" % (self.setup['file'])) configfile = open(self.setup['file'], 'r') rawconfig = configfile.read() configfile.close() except IOError: self.fatal_error("failed to read cached configuration from: %s" % (self.setup['file'])) else: # retrieve config from server proxy = Bcfg2.Client.Proxy.bcfg2() if self.setup['profile']: try: proxy.AssertProfile(self.setup['profile']) except xmlrpclib.Fault: self.fatal_error("Failed to set client profile") try: probe_data = proxy.GetProbes() except xmlrpclib.Fault: self.logger.error("Failed to download probes from bcfg2") raise SystemExit, 1 times['probe_download'] = time.time() try: probes = Bcfg2.Client.XML.XML(probe_data) except Bcfg2.Client.XML.ParseError, syntax_error: self.fatal_error( "server returned invalid probe requests: %s" % (syntax_error)) # execute probes try: probedata = Bcfg2.Client.XML.Element("ProbeData") [probedata.append(self.run_probe(probe)) for probe in probes.findall(".//probe")] except: self.logger.error("Failed to Execute probes") raise SystemExit, 1 try: # upload probe responses proxy.RecvProbeData(Bcfg2.Client.XML.tostring(probedata)) except: self.logger.error("Failed to upload probe data", exc_info=1) raise SystemExit, 1 times['probe_upload'] = time.time() try: rawconfig = proxy.GetConfig() except xmlrpclib.Fault: self.logger.error("Failed to download configuration from bcfg2") raise SystemExit, 2 times['config_download'] = time.time() if self.setup['cache']: try: open(self.setup['cache'], 'w').write(rawconfig) os.chmod(self.setup['cache'], 33152) except IOError: self.logger.warning("failed to write config cache file %s" % (self.setup['cache'])) times['caching'] = time.time() try: self.config = Bcfg2.Client.XML.XML(rawconfig) except Bcfg2.Client.XML.ParseError, syntax_error: self.fatal_error("the configuration could not be parsed: %s" % (syntax_error)) times['config_parse'] = time.time() if self.config.tag == 'error': self.fatal_error("server error: %s" % (self.config.text)) # Get toolset from server try: toolsetName = self.config.get('toolset', 'Toolset') except: self.fatal_error("server did not specify a toolset") if self.setup['bundle']: replacement_xml = Bcfg2.Client.XML.Element("Configuration", version='2.0') for child in self.config.getchildren(): if ((child.tag == 'Bundle') and (child.attrib['name'] == self.setup['bundle'])): replacement_xml.append(child) self.config = replacement_xml # Create toolset handle self.load_toolset(toolsetName) times['initialization'] = time.time() # verify state self.toolset.Inventory() times['inventory'] = time.time() # summarize current state self.toolset.CondDisplayState('initial') # install incorrect aspects of configuration self.toolset.Install() self.toolset.CondDisplayState('final') times['install'] = time.time() times['finished'] = time.time() if not self.setup['file'] and not self.setup['bundle']: # upload statistics feedback = Bcfg2.Client.XML.Element("upload-statistics") timeinfo = Bcfg2.Client.XML.Element("OpStamps") for (event, timestamp) in times.iteritems(): timeinfo.set(event, str(timestamp)) stats = self.toolset.GenerateStats(__revision__) stats.append(timeinfo) feedback.append(stats) try: proxy.RecvStats(Bcfg2.Client.XML.tostring(feedback)) except xmlrpclib.Fault: self.logger.error("Failed to upload configuration statistics") raise SystemExit, 2 if __name__ == '__main__': signal.signal(signal.SIGINT, cb_sigint_handler) Client().run()