#!/usr/bin/env python

'''Bcfg2 Client'''
__revision__ = '$Revision: 4015 $'

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)

def daemonize(filename):
    '''Do the double fork/setsession dance'''
    # Check if the pid is active
    try:
        pidfile = open(filename, "r")
        oldpid = int(pidfile.readline())
        # getpgid() will retun an IO error if all fails
        os.getpgid(oldpid)
        pidfile.close()

        # If we got this far without exceptions, there is another instance
        # running. Exit gracefully.
        print "PID File (%s) exists and listed PID (%d) is active." % \
              (filename, oldpid)
        raise SystemExit, 1
    except OSError:
        pidfile.close()
    except IOError: 
        # pid file doesn't
        pass

    # Fork once
    if os.fork() != 0:      
        os._exit(0)         
    os.setsid()                     # Create new session
    pid = os.fork()
    if pid != 0:
        pidfile = open(filename, "w")
        pidfile.write("%i" % pid)
        pidfile.close()
        os._exit(0)     
    os.chdir("/")         
    os.umask(0)

    null = open("/dev/null", "w+")

    os.dup2(null.fileno(), sys.__stdin__.fileno())
    os.dup2(null.fileno(), sys.__stdout__.fileno())
    os.dup2(null.fileno(), sys.__stderr__.fileno())

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', '<driver1>,<driver2>', "Specify tool driver set"),
                        False, ('client', 'drivers'), False, False),
            'fingerprint': (('-F', '<server fingerprint>', "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', '<bundle>', "only configure the given bundle"),
                       False, False, False, False),
            'file': (('-f', "<configspec>", "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', "<configspec>", "store the configuration in a file"),
                      False, False, False, False),
            'profile': (('-p', '<profile>', "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', '<configfile>', "use given config file (default /etc/bcfg2.conf)"),
                      False, False, '/etc/bcfg2.conf', False),
            'server': (('-S', '<server url>', 'the server hostname to connect to'),
                       False, ('components', 'bcfg2'), 'https://localhost:6789', False),
            'user': (('-u', '<user>', 'the user to provide for authentication'),
                     False, ('communication', 'user'), 'root', False),
            'password': (('-x', '<password>', 'the password to provide for authentication'),
                         False, ('communication', 'password'), 'password', False),
            'retries': (('-R', '<numretries>', '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', '<agent tcp port>', 'the port on which to bind for agent mode'),
                       False, ('communication', 'agent-port'), '6789', False),
            'agent-background': (('-i', '<pidfile name>', "Daemonize the agent"),
                                 False, False, False, False),
            'key': (('-K', '<client key file>', '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)
        if client.setup["agent-background"]:
            daemonize(client.setup["agent-background"])
        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()
