From e224ae0359840d54b9bd995a5231e4774321755a Mon Sep 17 00:00:00 2001 From: "Chris St. Pierre" Date: Fri, 28 Sep 2012 12:04:49 -0400 Subject: made client runs abort on probe failure, added option to disable that --- src/lib/Bcfg2/Options.py | 10 +- src/lib/Bcfg2/Server/Plugins/FileProbes.py | 27 +++- src/sbin/bcfg2 | 247 ++++++++++++++++------------- 3 files changed, 161 insertions(+), 123 deletions(-) (limited to 'src') diff --git a/src/lib/Bcfg2/Options.py b/src/lib/Bcfg2/Options.py index 04233f165..b5a116ddb 100644 --- a/src/lib/Bcfg2/Options.py +++ b/src/lib/Bcfg2/Options.py @@ -723,6 +723,13 @@ CLIENT_DECISION_LIST = \ cmd='--decision-list', odesc='', long_arg=True) +CLIENT_EXIT_ON_PROBE_FAILURE = \ + Option("The client should exit if a probe fails", + default=True, + cmd='--exit-on-probe-failure', + long_arg=True, + cf=('client', 'exit_on_probe_failure'), + cook=get_bool) # bcfg2-test and bcfg2-lint options TEST_NOSEOPTS = \ @@ -1079,7 +1086,8 @@ CLIENT_COMMON_OPTIONS = \ ca=CLIENT_CA, serverCN=CLIENT_SCNS, timeout=CLIENT_TIMEOUT, - decision_list=CLIENT_DECISION_LIST) + decision_list=CLIENT_DECISION_LIST, + probe_exit=CLIENT_EXIT_ON_PROBE_FAILURE) CLIENT_COMMON_OPTIONS.update(DRIVER_OPTIONS) CLIENT_COMMON_OPTIONS.update(CLI_COMMON_OPTIONS) diff --git a/src/lib/Bcfg2/Server/Plugins/FileProbes.py b/src/lib/Bcfg2/Server/Plugins/FileProbes.py index 59ac9f85e..b1930e203 100644 --- a/src/lib/Bcfg2/Server/Plugins/FileProbes.py +++ b/src/lib/Bcfg2/Server/Plugins/FileProbes.py @@ -13,9 +13,14 @@ import Bcfg2.Server import Bcfg2.Server.Plugin from Bcfg2.Compat import b64decode +#: The probe we send to clients to get the file data. Returns an XML +#: document describing the file and its metadata. We avoid returning +#: a non-0 error code on most errors, since that could halt client +#: execution. PROBECODE = """#!/usr/bin/env python import os +import sys import pwd import grp import Bcfg2.Client.XML @@ -24,16 +29,24 @@ from Bcfg2.Compat import b64encode path = "%s" if not os.path.exists(path): - print("%%s does not exist" %% path) - raise SystemExit(1) - -stat = os.stat(path) + sys.stderr.write("%%s does not exist" %% path) + raise SystemExit(0) + +try: + stat = os.stat(path) +except: + sys.stderr.write("Could not stat %%s: %%s" % (path, sys.exc_info()[1])) + raise SystemExit(0) data = Bcfg2.Client.XML.Element("ProbedFileData", name=path, owner=pwd.getpwuid(stat[4])[0], group=grp.getgrgid(stat[5])[0], - perms=oct(stat[0] & 07777)) -data.text = b64encode(open(path).read()) + perms=oct(stat[0] & 4095)) +try: + data.text = b64encode(open(path).read()) +except: + sys.stderr.write("Could not read %%s: %%s" % (path, sys.exc_info()[1])) + raise SystemExit(0) print(Bcfg2.Client.XML.tostring(data, xml_declaration=False).decode('UTF-8')) """ @@ -45,8 +58,6 @@ class FileProbes(Bcfg2.Server.Plugin.Plugin, replaced on the client if it is missing; if it has changed on the client, it can either be updated in the specification or replaced on the client """ - - name = 'FileProbes' __author__ = 'chris.a.st.pierre@gmail.com' def __init__(self, core, datastore): diff --git a/src/sbin/bcfg2 b/src/sbin/bcfg2 index f41479d77..8d8521b05 100755 --- a/src/sbin/bcfg2 +++ b/src/sbin/bcfg2 @@ -11,31 +11,30 @@ import stat import sys import tempfile import time +import Bcfg2.Proxy +import Bcfg2.Logger import Bcfg2.Options import Bcfg2.Client.XML import Bcfg2.Client.Frame import Bcfg2.Client.Tools -# Compatibility imports from Bcfg2.Compat import xmlrpclib - from Bcfg2.version import __version__ +from subprocess import Popen, PIPE -import Bcfg2.Proxy -import Bcfg2.Logger - -logger = logging.getLogger('bcfg2') def cb_sigint_handler(signum, frame): - """Exit upon CTRL-C.""" - os._exit(1) + """ Exit upon CTRL-C. """ + raise SystemExit(1) -class Client: +class Client(object): """The main bcfg2 client class""" def __init__(self): self.toolset = None + self.tools = None self.config = None + self._proxy = None optinfo = Bcfg2.Options.CLIENT_COMMON_OPTIONS self.setup = Bcfg2.Options.OptionParser(optinfo) @@ -82,6 +81,15 @@ class Client: if not self.setup['server'].startswith('https://'): self.setup['server'] = 'https://' + self.setup['server'] + def _probe_failure(self, probename, msg): + """ handle failure of a probe in the way the user wants us to + (exit or continue) """ + message = "Failed to execute probe %s: %s" % (probename, msg) + if self.setup['probe_exit']: + self.fatal_error(message) + else: + self.logger.error(message) + def run_probe(self, probe): """Execute probe.""" name = probe.get('name') @@ -91,78 +99,116 @@ class Client: source=probe.get('source')) try: scripthandle, scriptname = tempfile.mkstemp() - script = open(scriptname, 'w+') + script = os.fdopen(scripthandle, '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, + os.chmod(scriptname, 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() + proc = Popen(scriptname, stdin=PIPE, stdout=PIPE, stderr=PIPE) + ret.text, err = proc.communicate() + rv = proc.wait() + if err: + self.logger.warning("Probe %s has error output: %s" % + (name, err)) + if rv: + self._probe_failure(name, "Return value %s" % rv) self.logger.info("Probe %s has result:" % name) self.logger.info(ret.text) finally: - os.unlink(script.name) - except: - self.logger.error("Failed to execute probe: %s" % (name), exc_info=1) - raise SystemExit(1) + os.unlink(scriptname) + except: # pylint: disable=W0702 + self._probe_failure(name, sys.exc_info()[1]) return ret def fatal_error(self, message): """Signal a fatal error.""" self.logger.error("Fatal error: %s" % (message)) - os._exit(1) + raise SystemExit(1) + + @property + def proxy(self): + """ get an XML-RPC proxy to the server """ + if self._proxy is None: + self._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'], + retries=int(self.setup['retries']), + delay=int(self.setup['retry_delay'])) + return self._proxy + + def run_probes(self, times=None): + """ run probes and upload probe data """ + if times is None: + times = dict() - def run(self): - """Perform client execution phase.""" - times = {} + try: + probes = Bcfg2.Client.XML.XML(str(self.proxy.GetProbes())) + except (Bcfg2.Proxy.ProxyError, + Bcfg2.Proxy.CertificateError, + socket.gaierror, + socket.error): + self.fatal_error("Failed to download probes from bcfg2: %s" % err) + except Bcfg2.Client.XML.ParseError: + err = sys.exc_info()[1] + self.fatal_error("Server returned invalid probe requests: %s" % + err) - # begin configuration - times['start'] = time.time() + times['probe_download'] = time.time() - self.logger.info("Starting Bcfg2 client run at %s" % times['start']) + # execute probes + probedata = Bcfg2.Client.XML.Element("ProbeData") + for probe in probes.findall(".//probe"): + probedata.append(self.run_probe(probe)) + + if len(probes.findall(".//probe")) > 0: + try: + # upload probe responses + self.proxy.RecvProbeData(Bcfg2.Client.XML.tostring( + probedata, + xml_declaration=False).decode('UTF-8')) + except Bcfg2.Proxy.ProxyError: + err = sys.exc_info()[1] + self.fatal_error("Failed to upload probe data: %s" % err) + + times['probe_upload'] = time.time() + + def get_config(self, times=None): + """ load the configuration, either from the cached + configuration file (-f), or from the server """ + if times is None: + times = dict() 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() + self.setup['file']) + return open(self.setup['file'], 'r').read() 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'], - retries=int(self.setup['retries']), - delay=int(self.setup['retry_delay'])) - if self.setup['profile']: try: - proxy.AssertProfile(self.setup['profile']) + self.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) + self.fatal_error("Failed to set client profile: %s" % err) try: - probe_data = proxy.DeclareVersion(__version__) + self.proxy.DeclareVersion(__version__) except xmlrpclib.Fault: err = sys.exc_info()[1] if (err.faultCode == xmlrpclib.METHOD_NOT_FOUND or @@ -179,69 +225,38 @@ class Client: err = sys.exc_info()[1] self.logger.error("Failed to declare version: %s" % err) - 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(str(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, - xml_declaration=False).decode('UTF-8')) - 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() + self.run_probes(times=times) if self.setup['decision'] in ['whitelist', 'blacklist']: try: self.setup['decision_list'] = \ - proxy.GetDecisionList(self.setup['decision']) + self.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) + self.fatal_error("Failed to get decision list: %s" % err) try: - rawconfig = proxy.GetConfig().encode('UTF-8') + rawconfig = self.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) + self.fatal_error("Failed to download configuration from " + "Bcfg2: %s" % err) times['config_download'] = time.time() + return rawconfig + + def run(self): + """Perform client execution phase.""" + times = {} + + # begin configuration + times['start'] = time.time() + + self.logger.info("Starting Bcfg2 client run at %s" % times['start']) + + rawconfig = self.get_config(times=times) if self.setup['cache']: try: @@ -268,13 +283,13 @@ class Client: if self.setup['bundle_quick']: newconfig = Bcfg2.Client.XML.XML('') - [newconfig.append(bundle) - for bundle in self.config.getchildren() - if (bundle.tag == 'Bundle' and - ((self.setup['bundle'] and - bundle.get('name') in self.setup['bundle']) or - (self.setup['skipbundle'] and - bundle.get('name') not in self.setup['skipbundle'])))] + for bundle in self.config.getchildren(): + if (bundle.tag == 'Bundle' and + ((self.setup['bundle'] and + bundle.get('name') in self.setup['bundle']) or + (self.setup['skipbundle'] and + bundle.get('name') not in self.setup['skipbundle']))): + newconfig.append(bundle) self.config = newconfig self.tools = Bcfg2.Client.Frame.Frame(self.config, @@ -287,33 +302,38 @@ class Client: try: lockfile = open(self.setup['lockfile'], 'w') try: - fcntl.lockf(lockfile.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) + 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: + # 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: # pylint: disable=W0702 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 + # 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) + 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, - xml_declaration=False).decode('UTF-8')) + self.proxy.RecvStats(Bcfg2.Client.XML.tostring( + feedback, + xml_declaration=False).decode('UTF-8')) except Bcfg2.Proxy.ProxyError: err = sys.exc_info()[1] self.logger.error("Failed to upload configuration statistics: " @@ -322,8 +342,7 @@ class Client: self.logger.info("Finished Bcfg2 client run at %s" % time.time()) + if __name__ == '__main__': signal.signal(signal.SIGINT, cb_sigint_handler) - client = Client() - spid = os.getpid() - client.run() + Client().run() -- cgit v1.2.3-1-g7c22