#!/usr/bin/env python '''Bcfg2 Client''' __revision__ = '$Revision$' import getopt import logging import os import signal import socket import sys import tempfile import time import xmlrpclib import Bcfg2.Options import Bcfg2.Client.XML import Bcfg2.Client.Frame import Bcfg2.Client.Tools from Bcfg2.Component import * from Bcfg2.tlslite.Checker import Checker from Bcfg2.tlslite.errors import * try: import Bcfg2.Client.Proxy import 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), 'extra': (('-e', False, "enable extra entry 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), 'drivers': (('-D', ',', "Specify tool driver set"), False, ('client', 'drivers'), False, False), 'fingerprint': (('-F', '', "Server Fingerprint"), False, ('communication', 'fingerprint'), False, False), '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), 'interactive': (('-I', False, "prompt the user for each change"), False, False, False, True), '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), 'agent': (('-A', False, "run in agent (continuous) mode, wait for reconfigure command from server"), False, False, False, True), 'agent-port': (('-g', '', 'the port on which to bind for agent mode'), False, ('communication', 'agent-port'), '6789', False), 'key': (('-K', '', 'ssl cert + private key for agent mode xmlrpc server'), False, ('communication', 'key'), False, False), } optparser = Bcfg2.Options.OptionParser('bcfg2', optinfo) self.setup = optparser.parse() if getopt.getopt(sys.argv[1:], optparser.shortopt, optparser.longopt)[1]: print "Bcfg2 takes no arguments, only options" print optparser.helpmsg raise SystemExit(1) 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 '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, disable services to get former behavior") 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["agent"] != False) and (self.setup["interactive"] != False): print "cannot use -A and -I together" raise SystemExit(1) if (self.setup["agent"] and not self.setup["fingerprint"]): print "Agent mode requires specification of x509 fingerprint" raise SystemExit(1) if (self.setup["agent"] and not self.setup["key"]): print "Agent mode requires specification of ssl cert + key file" raise SystemExit(1) 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: 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() 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)) if not self.setup["agent"]: raise SystemExit(1) else: self.logger.error("Continuing...") 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 try: proxy = Bcfg2.Client.Proxy.bcfg2(self.setup) except: self.fatal_error("failed to instantiate proxy to server") return(1) if self.setup['profile']: try: proxy.AssertProfile(self.setup['profile']) except xmlrpclib.Fault: self.fatal_error("Failed to set client profile") return(1) try: probe_data = proxy.GetProbes() except xmlrpclib.Fault, flt: self.logger.error("Failed to download probes from bcfg2") self.logger.error(flt.faultString) 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)) 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)) 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)) return(1) times['config_parse'] = time.time() if self.config.tag == 'error': self.fatal_error("server error: %s" % (self.config.text)) return(1) self.tools = Bcfg2.Client.Frame.Frame(self.config, self.setup, times) self.tools.Execute() if not self.setup['file']: # upload statistics feedback = self.tools.GenerateStats() try: proxy.RecvStats(Bcfg2.Client.XML.tostring(feedback)) except xmlrpclib.Fault: self.logger.error("Failed to upload configuration statistics") raise SystemExit(2) class FingerCheck(object): def __init__(self, fprint): self.fingerprint = fprint self.logger = logging.getLogger('checker') def __call__(self, connection): if connection._client: chain = connection.session.serverCertChain else: chain = connection.session.clientCertChain if chain == None: self.logger.error("Fingerprint authentication error") raise TLSNoAuthenticationError() if chain.getFingerprint() != self.fingerprint: self.logger.error("Got connection with bad fingerprint %s" \ % (chain.getFingerprint())) raise TLSFingerprintError(\ "X.509 fingerprint mismatch: %s, %s" % \ (chain.getFingerprint(), self.fingerprint)) class Agent(Bcfg2.Component.Component): """The Bcfg2 Agent component providing XML-RPC access to 'run'""" __name__ = 'bcfg2-agent' __implementation__ = 'bcfg2-agent' def __init__(self, client): # need to get addr self.setup = client.setup self.shut = False signal.signal(signal.SIGINT, self.start_shutdown) signal.signal(signal.SIGTERM, self.start_shutdown) self.logger = logging.getLogger('Agent') self.static = True if self.setup["agent-port"]: port = int(self.setup["agent-port"]) elif self.setup["server"]: port = int(self.setup["server"].split(':')[1]) else: print "port or server URL not specified" raise SystemExit, 1 location = (socket.gethostname(), port) keyfile = self.setup["key"] self.password = self.setup["password"] try: TLSServer.__init__(self, location, keyfile, CobaltXMLRPCRequestHandler, FingerCheck(self.setup["fingerprint"]), reqCert=True) except socket.error: self.logger.error("Failed to bind to socket") raise ComponentInitError except ComponentKeyError: self.logger.error("Failed to parse key" % (keyfile)) raise ComponentInitError except: self.logger.error("Failed to load ssl key %s" % (keyfile), exc_info=1) raise ComponentInitError try: SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self) except TypeError: SimpleXMLRPCServer.SimpleXMLRPCDispatcher.__init__(self, False, None) self.logRequests = 0 self.port = self.socket.getsockname()[1] self.url = "https://%s:%s" % (socket.gethostname(), self.port) self.logger.info("Bound to port %s" % self.port) self.funcs.update({'system.listMethods':self.addr_system_listMethods}) self.atime = 0 self.client = client self.funcs.update({ "run": self.run, }) def run(self, address): try: os.waitpid(-1, os.WNOHANG) except: pass self.logger.info("Got run request from %s" % (address[0])) if os.fork(): return True else: try: self.client.run() except SystemExit: self.logger.error("Client failed to execute") self.shut = True return False if __name__ == '__main__': signal.signal(signal.SIGINT, cb_sigint_handler) client = Client() spid = os.getpid() if client.setup["agent"]: agent = Agent(client) while not agent.shut: try: agent.serve_forever() except: critical_error('error in service loop') if os.getpid() == spid: print("Shutting down") else: client.run()