summaryrefslogtreecommitdiffstats
path: root/src/lib/Bcfg2/Client/Client.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Bcfg2/Client/Client.py')
-rw-r--r--src/lib/Bcfg2/Client/Client.py327
1 files changed, 327 insertions, 0 deletions
diff --git a/src/lib/Bcfg2/Client/Client.py b/src/lib/Bcfg2/Client/Client.py
new file mode 100644
index 000000000..ed1333fb8
--- /dev/null
+++ b/src/lib/Bcfg2/Client/Client.py
@@ -0,0 +1,327 @@
+import os
+import sys
+import stat
+import time
+import fcntl
+import socket
+import logging
+import tempfile
+import Bcfg2.Proxy
+import Bcfg2.Logger
+import Bcfg2.Options
+import Bcfg2.Client.XML
+import Bcfg2.Client.Frame
+import Bcfg2.Client.Tools
+from Bcfg2.Compat import xmlrpclib
+from Bcfg2.version import __version__
+from subprocess import Popen, PIPE
+
+
+class Client(object):
+ """The main bcfg2 client class"""
+
+ def __init__(self, setup):
+ self.toolset = None
+ self.tools = None
+ self.config = None
+ self._proxy = None
+ self.setup = setup
+
+ if self.setup['debug']:
+ level = logging.DEBUG
+ elif self.setup['verbose']:
+ level = logging.INFO
+ else:
+ level = logging.WARNING
+ Bcfg2.Logger.setup_logging('bcfg2',
+ to_syslog=self.setup['syslog'],
+ level=level,
+ to_file=self.setup['logging'])
+ self.logger = logging.getLogger('bcfg2')
+ self.logger.debug(self.setup)
+ if self.setup['bundle_quick']:
+ if not self.setup['bundle'] and not self.setup['skipbundle']:
+ self.logger.error("-Q option requires -b or -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 _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')
+ 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 = os.fdopen(scripthandle, 'w')
+ try:
+ script.write("#!%s\n" %
+ (probe.attrib.get('interpreter', '/bin/sh')))
+ script.write(probe.text)
+ script.close()
+ 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
+ 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(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))
+ 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()
+
+ try:
+ probes = Bcfg2.Client.XML.XML(str(self.proxy.GetProbes()))
+ except (Bcfg2.Proxy.ProxyError,
+ Bcfg2.Proxy.CertificateError,
+ socket.gaierror,
+ socket.error):
+ err = sys.exc_info()[1]
+ 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)
+
+ times['probe_download'] = time.time()
+
+ # 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'])
+ return open(self.setup['file'], 'r').read()
+ except IOError:
+ self.fatal_error("Failed to read cached configuration from: %s"
+ % (self.setup['file']))
+ else:
+ # retrieve config from server
+ if self.setup['profile']:
+ try:
+ self.proxy.AssertProfile(self.setup['profile'])
+ except Bcfg2.Proxy.ProxyError:
+ err = sys.exc_info()[1]
+ self.fatal_error("Failed to set client profile: %s" % err)
+
+ try:
+ self.proxy.DeclareVersion(__version__)
+ except xmlrpclib.Fault:
+ err = sys.exc_info()[1]
+ if (err.faultCode == xmlrpclib.METHOD_NOT_FOUND or
+ (err.faultCode == 7 and
+ err.faultString.startswith("Unknown method"))):
+ self.logger.debug("Server does not support declaring "
+ "client version")
+ else:
+ self.logger.error("Failed to declare version: %s" % err)
+ except (Bcfg2.Proxy.ProxyError,
+ Bcfg2.Proxy.CertificateError,
+ socket.gaierror,
+ socket.error):
+ err = sys.exc_info()[1]
+ self.logger.error("Failed to declare version: %s" % err)
+
+ self.run_probes(times=times)
+
+ if self.setup['decision'] in ['whitelist', 'blacklist']:
+ try:
+ self.setup['decision_list'] = \
+ 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.fatal_error("Failed to get decision list: %s" % err)
+
+ try:
+ rawconfig = self.proxy.GetConfig().encode('UTF-8')
+ except Bcfg2.Proxy.ProxyError:
+ err = sys.exc_info()[1]
+ 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:
+ 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('<Configuration/>')
+ 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,
+ 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: # 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
+ 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:
+ 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: "
+ "%s" % err)
+ raise SystemExit(2)
+
+ self.logger.info("Finished Bcfg2 client run at %s" % time.time())