#!/usr/bin/env python """Bcfg2 Client""" import fcntl import logging import os import signal import socket import stat import sys import tempfile import time import Bcfg2.Options import Bcfg2.Client.XML import Bcfg2.Client.Frame import Bcfg2.Client.Tools # Compatibility imports from Bcfg2.Bcfg2Py3k import xmlrpclib import Bcfg2.Proxy import Bcfg2.Logger logger = logging.getLogger('bcfg2') 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 = \ dict(extra=Bcfg2.Options.CLIENT_EXTRA_DISPLAY, quick=Bcfg2.Options.CLIENT_QUICK, lockfile=Bcfg2.Options.LOCKFILE, drivers=Bcfg2.Options.CLIENT_DRIVERS, dryrun=Bcfg2.Options.CLIENT_DRYRUN, paranoid=Bcfg2.Options.CLIENT_PARANOID, bundle=Bcfg2.Options.CLIENT_BUNDLE, skipbundle=Bcfg2.Options.CLIENT_SKIPBUNDLE, bundle_quick=Bcfg2.Options.CLIENT_BUNDLEQUICK, indep=Bcfg2.Options.CLIENT_INDEP, skipindep=Bcfg2.Options.CLIENT_SKIPINDEP, file=Bcfg2.Options.CLIENT_FILE, interactive=Bcfg2.Options.INTERACTIVE, cache=Bcfg2.Options.CLIENT_CACHE, profile=Bcfg2.Options.CLIENT_PROFILE, remove=Bcfg2.Options.CLIENT_REMOVE, server=Bcfg2.Options.SERVER_LOCATION, user=Bcfg2.Options.CLIENT_USER, password=Bcfg2.Options.SERVER_PASSWORD, retries=Bcfg2.Options.CLIENT_RETRIES, kevlar=Bcfg2.Options.CLIENT_KEVLAR, omit_lock_check=Bcfg2.Options.OMIT_LOCK_CHECK, decision=Bcfg2.Options.CLIENT_DLIST, servicemode=Bcfg2.Options.CLIENT_SERVICE_MODE, key=Bcfg2.Options.CLIENT_KEY, certificate=Bcfg2.Options.CLIENT_CERT, ca=Bcfg2.Options.CLIENT_CA, serverCN=Bcfg2.Options.CLIENT_SCNS, timeout=Bcfg2.Options.CLIENT_TIMEOUT, decision_list=Bcfg2.Options.CLIENT_DECISION_LIST) optinfo.update(Bcfg2.Options.CLI_COMMON_OPTIONS) optinfo.update(Bcfg2.Options.DRIVER_OPTIONS) self.setup = Bcfg2.Options.OptionParser(optinfo) self.setup.parse(sys.argv[1:]) if self.setup['args']: print("Bcfg2 takes no arguments, only options") print(self.setup.buildHelpMessage()) raise SystemExit(1) level = 30 if self.setup['verbose']: level = 20 if self.setup['debug']: level = 0 Bcfg2.Logger.setup_logging('bcfg2', to_syslog=False, level=level, to_file=self.setup['logging']) self.logger = logging.getLogger('bcfg2') self.logger.debug(self.setup) if self.setup['bundle_quick']: if self.setup['bundle'] == []: self.logger.error("-Q option requires -b") raise SystemExit(1) elif self.setup['remove']: self.logger.error("-Q option incompatible with -r") raise SystemExit(1) if 'drivers' in self.setup and self.setup['drivers'] == 'help': self.logger.info("The following drivers are available:") self.logger.info(Bcfg2.Client.Tools.drivers) raise SystemExit(0) if self.setup['remove'] and 'services' in self.setup['remove'].lower(): self.logger.error("Service removal is nonsensical; " "removed services will only be disabled") if (self.setup['remove'] and self.setup['remove'].lower() not in ['all', 'services', 'packages']): self.logger.error("Got unknown argument %s for -r" % self.setup['remove']) if self.setup["file"] and self.setup["cache"]: print("cannot use -f and -c together") raise SystemExit(1) if not self.setup['server'].startswith('https://'): self.setup['server'] = 'https://' + self.setup['server'] def run_probe(self, probe): """Execute probe.""" name = probe.get('name') self.logger.info("Running probe %s" % name) ret = Bcfg2.Client.XML.Element("probe-data", name=name, source=probe.get('source')) try: scripthandle, scriptname = tempfile.mkstemp() script = open(scriptname, 'w+') try: script.write("#!%s\n" % (probe.attrib.get('interpreter', '/bin/sh'))) script.write(probe.text) script.close() os.close(scripthandle) os.chmod(script.name, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH | stat.S_IWUSR) # 0755 ret.text = os.popen(script.name).read().strip() self.logger.info("Probe %s has result:\n%s" % (name, ret.text)) 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)) os._exit(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'])) return(1) else: # retrieve config from server proxy = Bcfg2.Proxy.ComponentProxy(self.setup['server'], self.setup['user'], self.setup['password'], key=self.setup['key'], cert=self.setup['certificate'], ca=self.setup['ca'], allowedServerCNs=self.setup['serverCN'], timeout=self.setup['timeout']) if self.setup['profile']: try: proxy.AssertProfile(self.setup['profile']) except Bcfg2.Proxy.ProxyError: err = sys.exc_info()[1] self.fatal_error("Failed to set client profile") self.logger.error(str(err)) raise SystemExit(1) try: probe_data = proxy.GetProbes() except (Bcfg2.Proxy.ProxyError, Bcfg2.Proxy.CertificateError, socket.gaierror, socket.error): err = sys.exc_info()[1] self.logger.error("Failed to download probes from bcfg2: %s" % err) raise SystemExit(1) times['probe_download'] = time.time() try: probes = Bcfg2.Client.XML.XML(probe_data) except Bcfg2.Client.XML.ParseError: syntax_error = sys.exc_info()[1] self.fatal_error( "Server returned invalid probe requests: %s" % (syntax_error)) return(1) # 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) if len(probes.findall(".//probe")) > 0: try: # upload probe responses proxy.RecvProbeData(Bcfg2.Client.XML.tostring(probedata, encoding='UTF-8', xml_declaration=True)) except Bcfg2.Proxy.ProxyError: err = sys.exc_info()[1] self.logger.error("Failed to upload probe data: %s" % err) raise SystemExit(1) times['probe_upload'] = time.time() if self.setup['decision'] in ['whitelist', 'blacklist']: try: self.setup['decision_list'] = \ proxy.GetDecisionList(self.setup['decision']) self.logger.info("Got decision list from server:") self.logger.info(self.setup['decision_list']) except Bcfg2.Proxy.ProxyError: err = sys.exc_info()[1] self.logger.error("Failed to get decision list: %s" % err) raise SystemExit(1) try: rawconfig = proxy.GetConfig().encode('UTF-8') except Bcfg2.Proxy.ProxyError: err = sys.exc_info()[1] self.logger.error("Failed to download configuration from " "Bcfg2: %s" % err) 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 = sys.exc_info()[1] self.fatal_error("The configuration could not be parsed: %s" % (syntax_error)) return(1) times['config_parse'] = time.time() if self.config.tag == 'error': self.fatal_error("Server error: %s" % (self.config.text)) return(1) if self.setup['bundle_quick']: newconfig = Bcfg2.Client.XML.XML('') [newconfig.append(bundle) for bundle in self.config.getchildren() if (bundle.tag == 'Bundle' and bundle.get('name') in self.setup['bundle'])] self.config = newconfig self.tools = Bcfg2.Client.Frame.Frame(self.config, self.setup, times, self.setup['drivers'], self.setup['dryrun']) if not self.setup['omit_lock_check']: #check lock here try: lockfile = open(self.setup['lockfile'], 'w') try: fcntl.lockf(lockfile.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) except IOError: #otherwise exit and give a warning to the user self.fatal_error("An other instance of Bcfg2 is running. If you what to bypass the check, run with %s option" % (Bcfg2.Options.OMIT_LOCK_CHECK.cmd)) except: lockfile = None self.logger.error("Failed to open lockfile") # execute the said configuration self.tools.Execute() if not self.setup['omit_lock_check']: #unlock here if lockfile: try: fcntl.lockf(lockfile.fileno(), fcntl.LOCK_UN) os.remove(self.setup['lockfile']) except OSError: self.logger.error("Failed to unlock lockfile %s" % lockfile.name) if not self.setup['file'] and not self.setup['bundle_quick']: # upload statistics feedback = self.tools.GenerateStats() try: proxy.RecvStats(Bcfg2.Client.XML.tostring(feedback, encoding='UTF-8', xml_declaration=True)) except Bcfg2.Proxy.ProxyError: err = sys.exc_info()[1] self.logger.error("Failed to upload configuration statistics: " "%s" % err) raise SystemExit(2) if __name__ == '__main__': signal.signal(signal.SIGINT, cb_sigint_handler) client = Client() spid = os.getpid() client.run()