#!/usr/bin/env python

'''The XML-RPC Bcfg2 Server'''
__revision__ = '$Revision: 2428 $'

import Bcfg2.Server.Metadata 

from Bcfg2.Server.Core import Core, CoreInitError
from xmlrpclib import Fault
from lxml.etree import XML, Element, tostring

import logging, os, select, signal, socket, sys
import Bcfg2.Logging, Bcfg2.Options, Bcfg2.Server.Component

logger = logging.getLogger('bcfg2-server')

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.
        logger.critical("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())

def critical_error(operation):
    '''Log and err, traceback and return an xmlrpc fault to client'''
    logger.error(operation, exc_info=1)
    raise Fault, (7, "Critical unexpected failure: %s" % (operation))

class Bcfg2Serv(Bcfg2.Server.Component.Component):
    """The Bcfg2 Server component providing XML-RPC access to Bcfg methods"""
    __name__ = 'bcfg2'
    __implementation__ = 'bcfg2'

    request_queue_size = 15

    def __init__(self, setup):
        try:
            Bcfg2.Server.Component.Component.__init__(self, setup)
            self.shut = False
        except Bcfg2.Server.Component.ComponentInitError:
            logger.critical("Failed to setup server")
            raise SystemExit, 1

        # set shutdown handlers for sigint and sigterm
        signal.signal(signal.SIGINT, self.start_shutdown)
        signal.signal(signal.SIGTERM, self.start_shutdown)
        try:
            self.Core = Core(setup, setup['configfile'])
        except CoreInitError, msg:
            logger.critical("Fatal error: %s" % (msg))
            raise SystemExit, 1

        self.funcs.update({
            "AssertProfile": self.Bcfg2AssertProfile, 
            "GetConfig": self.Bcfg2GetConfig,
            "GetProbes": self.Bcfg2GetProbes,
            "RecvProbeData": self.Bcfg2RecvProbeData,
            "RecvStats": self.Bcfg2RecvStats
            })
        for plugin in self.Core.plugins.values():
            for method in plugin.__rmi__:
                self.register_function(getattr(self.Core.plugins[plugin.__name__], method),
                                       "%s.%s" % (plugin.__name__, method))

    def get_request(self):
        '''We need to do work between requests, so select with timeout instead of blocking in accept'''
        rsockinfo = []
        famfd = self.Core.fam.fileno()
        while self.socket not in rsockinfo:
            if self.shut:
                raise socket.error
            try:
                rsockinfo = select.select([self.socket, famfd], [], [], 15)[0]
            except select.error:
                continue
            
            if famfd in rsockinfo:
                self.Core.Service()
            if self.socket in rsockinfo:
                return self.socket.accept()

    def Bcfg2GetProbes(self, address):
        '''Fetch probes for a particular client'''
        resp = Element('probes')
        try:
            name = self.Core.metadata.resolve_client(address[0])
            meta = self.Core.metadata.get_metadata(name)
            
            for generator in self.Core.generators:
                for probe in generator.GetProbes(meta):
                    resp.append(probe)
            return tostring(resp)
        except Bcfg2.Server.Metadata.MetadataConsistencyError:
            warning = 'metadata consistency error'
            self.logger.warning(warning)
            raise Fault, (6, warning)
        except:
            critical_error("error determining client probes")

    def Bcfg2RecvProbeData(self, address, probedata):
        '''Receive probe data from clients'''
        try:
            name = self.Core.metadata.resolve_client(address[0])
            meta = self.Core.metadata.get_metadata(name)
        except Bcfg2.Server.Metadata.MetadataConsistencyError:
            warning = 'metadata consistency error'
            self.logger.warning(warning)
            raise Fault, (6, warning)
        try:
            xpdata = XML(probedata)
        except:
            self.logger.error("Failed to parse probe data from client %s" % (address[0]))
            return False

        for data in xpdata:
            if self.Core.plugins.has_key(data.get('source')):
                try:
                    self.Core.plugins[data.get('source')].ReceiveData(meta, data)
                except:
                    self.logger.error("Failed to process probe data from client %s" % (address), exc_info=1)
            else:
                self.logger.warning("Failed to locate plugin %s" % (data.get('source')))
        return True

    def Bcfg2AssertProfile(self, address, profile):
        '''Set profile for a client'''
        try:
            client = self.Core.metadata.resolve_client(address[0])
            self.Core.metadata.set_profile(client, profile)
        except (Bcfg2.Server.Metadata.MetadataConsistencyError, Bcfg2.Server.Metadata.MetadataRuntimeError):
            warning = 'metadata consistency error'
            self.logger.warning(warning)
            raise Fault, (6, warning)
        return True

    def Bcfg2GetConfig(self, address, _=False, profile=False):
        '''Build config for a client'''
        try:
            client = self.Core.metadata.resolve_client(address[0])
            return tostring(self.Core.BuildConfiguration(client))
        except Bcfg2.Server.Metadata.MetadataConsistencyError:
            self.logger.warning("Metadata consistency failure for %s" % (address))
            raise Fault, (6, "Metadata consistency failure")

    def Bcfg2RecvStats(self, address, stats):
        '''Act on statistics upload'''
        sdata = XML(stats)
        state = sdata.find(".//Statistics")
        # Versioned stats to prevent tied client/server upgrade
        if state.get('version') >= '2.0':
            client = self.Core.metadata.resolve_client(address[0])
            meta = self.Core.metadata.get_metadata(client)
            
            # Update statistics
            self.Core.stats.updateStats(sdata, meta.hostname)

        self.logger.info("Client %s reported state %s" % 
                         (client, state.attrib['state']))
        return "<ok/>"

if __name__ == '__main__':

    OPTINFO = {
        'verbose': (('-v', False, 'enable verbose output'),
                    False, False, False, True),
        'debug': (('-d', False, 'enable debugging output'),
                  False, False, False, True),
        'help': (('-h', False, 'display this usage information'),
                 False, False, False, True),
        'daemon': (('-D', '<pidfile>', 'daemonize the server, storing PID'),
                   False, False, False, False),
        'configfile': (('-C', '<conffile>', 'use this config file'),
                       False, False, '/etc/bcfg2.conf', False),
        }
    
    SSETUP = Bcfg2.Options.OptionParser('bcfg2', OPTINFO).parse()
    level = 0
    if '-D' in sys.argv:
        Bcfg2.Logging.setup_logging('bcfg2-server', to_console=False, level=level)
    else:
        Bcfg2.Logging.setup_logging('bcfg2-server', level=level)
    if SSETUP['daemon']:
        daemonize(SSETUP['daemon'])
    try:
        BSERV = Bcfg2Serv(SSETUP)
    except:
        critical_error("Failed to setup server; probably a key problem")
    while not BSERV.shut:
        try:
            BSERV.serve_forever()
        except:
            critical_error('error in service loop')
    logger.info("Shutting down")
