#!/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) DECISION_LIST = Bcfg2.Options.Option('Decision List', default=False, cmd="--decision-list", odesc='', long_arg=True) 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': Bcfg2.Options.VERBOSE, 'extra': Bcfg2.Options.CLIENT_EXTRA_DISPLAY, 'quick': Bcfg2.Options.CLIENT_QUICK, 'debug': Bcfg2.Options.DEBUG, 'lockfile': Bcfg2.Options.LOCKFILE, 'drivers': Bcfg2.Options.CLIENT_DRIVERS, 'dryrun': Bcfg2.Options.CLIENT_DRYRUN, 'paranoid': Bcfg2.Options.CLIENT_PARANOID, 'bundle': Bcfg2.Options.CLIENT_BUNDLE, 'bundle-quick': Bcfg2.Options.CLIENT_BUNDLEQUICK, 'indep': Bcfg2.Options.CLIENT_INDEP, 'file': Bcfg2.Options.CLIENT_FILE, 'interactive': Bcfg2.Options.INTERACTIVE, 'cache': Bcfg2.Options.CLIENT_CACHE, 'profile': Bcfg2.Options.CLIENT_PROFILE, 'remove': Bcfg2.Options.CLIENT_REMOVE, 'help': Bcfg2.Options.HELP, 'setup': Bcfg2.Options.CFILE, '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, 'decision-list': DECISION_LIST, 'encoding': Bcfg2.Options.ENCODING, 'omit-lock-check': Bcfg2.Options.OMIT_LOCK_CHECK, 'filelog': Bcfg2.Options.LOGGING_FILE_PATH, '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, } 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['filelog']) 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'] != False: 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']: self.logger.error("Service removal is nonsensical; removed services will only be disabled") if self.setup['remove'] not in [False, 'all', 'Services', 'Packages', '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 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()