#!/usr/bin/python
# -*- coding: utf-8; -*-
#
# (c) 2007-2008 Mandriva, http://www.mandriva.com/
#
# $Id$
#
# This file is part of Pulse 2, http://pulse2.mandriva.org
#
# Pulse 2 is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Pulse 2 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Pulse 2. If not, see <http://www.gnu.org/licenses/>.
#

# Big modules
import os
import sys
import optparse
import xmlrpclib
import time
import random

# Twisted
from twisted.internet import epollreactor
epollreactor.install()
import twisted.web.xmlrpc
import twisted.web.server
import twisted.internet.defer
try:
    from twisted.web import http
except ImportError:
    from twisted.protocols import http

# Other stuff
from mmc.site import mmcconfdir
from pulse2.version import getVersion, getRevision
from pulse2.launcher.config import LauncherConfig
from pulse2.utils import xmlrpcCleanup
import pulse2.launcher.utils
import pulse2.launcher.ssh_agent
# XMLRPC funcs
import pulse2.launcher.remote_exec
import pulse2.launcher.process_control
import pulse2.launcher.network
import pulse2.launcher.tcp_sproxy

from pulse2.launcher.xmlrpc import LauncherSite, getProxy
from pulse2.launcher.health import getHealth
from pulse2.launcher.stats import getBalance
import pulse2.xmlrpc

# Logging
import logging
import logging.config

CB_STARTUP_DELAY = 2
LOG_STATS_DELAY = 60

def start_all_callbacks():
    logger = logging.getLogger()
    logger.info('launcher %s: will run callbacks in %d seconds' % (LauncherConfig().name, CB_STARTUP_DELAY))
    time.sleep(CB_STARTUP_DELAY)
    scheduleTellIAmAlive()
    scheduleLogStats()
    logger.info('launcher %s: all callbacks started' % (LauncherConfig().name))

def scheduleTellIAmAlive():
    """
        periodicaly talk to our referent scheduler
        attempt to awake every when time modulo awake_time equals 0
        f.ex., with awake_time equals to 10 minutes (600 seconds), will
        wake-up at 00:10:00, 00:20:00, 00:30:00 etc ...
        awake-time is completed with a random value of more or less
        awake_incertitude_factor
        f.ex., with awake_incertitude_factor set to .2 (20 %), will
        wake-up at 00:11:00 +/- 1 minutes, 00:21:00 +/- 1 minutes etc ...
    """
    # current time (epoch)
    current_time = time.time()
    # second after previous 'official' time
    next_wakeup_in = current_time % LauncherConfig().awake_time
    # second before next 'official' time
    next_wakeup_in = LauncherConfig().awake_time - next_wakeup_in
    # add some randomness
    next_wakeup_in = int(next_wakeup_in + random.random() * LauncherConfig().awake_incertitude_factor * LauncherConfig().awake_time)
    # schedule the next event
    twisted.internet.reactor.callLater(next_wakeup_in, tellIAmAlive)

def tellIAmAlive():
    def _cb(result): # scheduler do answer us, check if there is some zombies to expunge around
        if pulse2.launcher.process_control.ProcessList().getZombiesCount() > 0:
            logging.getLogger().info('launcher %s: sending available results to our scheduler at %s' % (LauncherConfig().name, scheduler))
            for id in pulse2.launcher.process_control.ProcessList().getZombieIds():
                pulse2.launcher.process_control.ProcessList().getProcess(id).installEndBack()

    def _eb(reason): # can't contact scheduler, log and continue
        logging.getLogger().error('launcher %s: failed to contact our scheduler at %s: %s' % (LauncherConfig().name, scheduler, reason.value))

    # retrieve referent scheduler
    scheduler = pulse2.launcher.utils.getScheduler()

    # salute our referent scheduler
    mydeffered = getProxy(scheduler).callRemote('tell_i_am_alive', LauncherConfig().name)

    mydeffered.addCallback(_cb).addErrback(_eb)
    scheduleTellIAmAlive()

def scheduleLogStats():
    """ periodicaly log stats """
    delay = LOG_STATS_DELAY - time.time() % LOG_STATS_DELAY
    twisted.internet.reactor.callLater(delay, logStats)

def logStats():
    config = LauncherConfig().launchers[LauncherConfig().name]
    logging.getLogger().debug('launcher %s: BALANCE: %s' % (LauncherConfig().name, getBalance(config)))
    logging.getLogger().debug('launcher %s: HEALTH: %s' % (LauncherConfig().name, getHealth(config)))
    logging.getLogger().debug('launcher %s: PROCESSING: %s' % (LauncherConfig().name, pulse2.launcher.process_control.getProcessIds()))
    scheduleLogStats()

def startService():
    logger = logging.getLogger()
    config = LauncherConfig()

    name = config.name

    if not name in config.launchers:
        logger.error('launcher %s: No section / invalid section %s in config file !' % (name, name))
        return 1

    # put the scheduler vars
    config = LauncherConfig()
    scheduler = config.schedulers[config.launchers[config.name]['scheduler']]

    config.awake_time = scheduler['awake_time']
    config.awake_incertitude_factor = scheduler['awake_incertitude_factor']
    config.defer_results = scheduler['defer_results']
    config.scheduler_enablessl = scheduler['enablessl']
    config.scheduler_host = scheduler['host']
    config.scheduler_password = scheduler['password']
    config.scheduler_port = scheduler['port']
    config.scheduler_username = scheduler['username']

    port = int(config.launchers[config.name]['port'])
    bind = config.launchers[config.name]['bind']
    enablessl = config.launchers[config.name]['enablessl']
    slots = config.launchers[config.name]['slots']
    max_command_age = config.max_command_age

    # Starts XMLRPC server
    launcher = Launcher(name)
    try:
        if enablessl:
            pulse2.xmlrpc.OpenSSLContext().setup(
                config.launchers[config.name]['localcert'],
                config.launchers[config.name]['cacert'],
                config.launchers[config.name]['verifypeer']
            )
            twisted.internet.reactor.listenSSL(
                port,
                LauncherSite(launcher),
                interface = bind,
                contextFactory = pulse2.xmlrpc.OpenSSLContext().getContext()
                )
            logger.info('launcher %s: activating SSL mode' % (name))
        else:
            twisted.internet.reactor.listenTCP(
                port,
                twisted.web.server.Site(launcher),
                interface = bind
                )
    except Exception, e:
        logger.error('launcher %s: can\'t bind to %s:%d, reason is %s' % (name, bind, port, e))
        return 1

    pulse2.launcher.process_control.ProcessList().setup(slots, max_command_age)
    twisted.internet.reactor.callWhenRunning(start_all_callbacks)
    twisted.internet.reactor.addSystemEventTrigger('before', 'shutdown', cleanUp)
    logger.info('launcher %s: %d slots, listening on %s:%d' % (name, slots, bind, port))
    twisted.internet.reactor.run()
    return 0

def cleanUp():
    logger = logging.getLogger()
    logger.info('launcher %s: Shuting down and cleaning up' % (LauncherConfig().name))
    pulse2.launcher.ssh_agent.killSSHAgent()
    logger.info('launcher %s: End' % (LauncherConfig().name))

class Launcher(twisted.web.xmlrpc.XMLRPC):
    """Our launcher
    """

    def __init__(self, name):
        twisted.web.xmlrpc.XMLRPC.__init__(self)
        self.logger = logging.getLogger()
        self.logger.info("Launcher version('%s') build('%s')" % (str(getVersion()), str(getRevision())))
        self.name = name
        self.config = LauncherConfig().launchers[name]

    def _ebRender(self, failure):
        self.logger.error(failure)
        if isinstance(failure.value, xmlrpclib.Fault):
            return failure.value
        return xmlrpclib.Fault(self.FAILURE, "Internal Error on launcher %s" % self.name)

    def _cbRender(self, result, request):
        args, func = xmlrpclib.loads(request.content.getvalue())
        if isinstance(result, Launcher):
            result = xmlrpcCleanup(result.result)
        if not isinstance(result, xmlrpclib.Fault):
            result = (result,)
        self.logger.debug('launcher %s: xmlrpc: %s%s => %s' % (self.name, func, (args), (result)))
        try:
            s = xmlrpclib.dumps(result, methodresponse=1)
        except:
            f = xmlrpclib.Fault(self.FAILURE, "can't serialize output")
            s = xmlrpclib.dumps(f, methodresponse=1)
        request.setHeader("content-length", str(len(s)))
        request.write(s)
        request.finish()

    def render(self, request):
        """
        override method of xmlrpc python twisted framework
        """
        headers = request.getAllHeaders()
        args, functionPath = xmlrpclib.loads(request.content.read())
        # Twisted: deprecated private method '_getFunction' becomes
        #          public and named 'lookupProcedure'
        #          (changed since version 11.1)
        if hasattr(self, "lookupProcedure") :
            function = self.lookupProcedure(functionPath)
        elif hasattr(self, "_getFunction") :
            function = self._getFunction(functionPath)
        else :
            self.logger.error("Unable to get XML-RPC function '%s'" % functionPath)




        cleartext_token = '%s:%s' % (self.config['username'], self.config['password'])
        token = '%s:%s' % (request.getUser(), request.getPassword())
        if token != cleartext_token:
            self.logger.error("Invalid login / password for HTTP basic authentication")
            request.setResponseCode(http.UNAUTHORIZED)
            self._cbRender(
                twisted.web.xmlrpc.Fault(http.UNAUTHORIZED, "Unauthorized: invalid credentials to connect to this Pulse 2 Launcher, basic HTTP authentication is required"),
                request
                )
            return twisted.web.server.NOT_DONE_YET

        request.setHeader("content-type", "text/xml")
        twisted.internet.defer.maybeDeferred(function, *args).addErrback(
            self._ebRender
        ).addCallback(
            self._cbRender, request
        )
        return twisted.web.server.NOT_DONE_YET

    # Synchrone commands summoning
    def xmlrpc_sync_remote_push(self, command_id, client, files_list, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.sync_remote_push(int(command_id), client, files_list, timeout or LauncherConfig().wrapper_max_exec_time))
    def xmlrpc_sync_remote_pull(self, command_id, client, files_list, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.sync_remote_pull(int(command_id), client, files_list, timeout or LauncherConfig().wrapper_max_exec_time))
    def xmlrpc_sync_remote_delete(self, command_id, client, files_list, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.sync_remote_delete(int(command_id), client, files_list, timeout or LauncherConfig().wrapper_max_exec_time))
    def xmlrpc_sync_remote_exec(self, command_id, client, command, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.sync_remote_exec(int(command_id), client, command, timeout or LauncherConfig().wrapper_max_exec_time))
    def xmlrpc_sync_remote_quickaction(self, command_id, client, command, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.sync_remote_quickaction(int(command_id), client, command, timeout or LauncherConfig().wrapper_max_exec_time))
    def xmlrpc_sync_remote_inventory(self, command_id, client, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.sync_remote_inventory(int(command_id), client, timeout or LauncherConfig().wrapper_max_exec_time))
    def xmlrpc_sync_remote_reboot(self, command_id, client, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.sync_remote_reboot(int(command_id), client, timeout or LauncherConfig().wrapper_max_exec_time))
    def xmlrpc_sync_remote_lock_reboot(self, command_id, client, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.sync_remote_lock_reboot(int(command_id), client, timeout or LauncherConfig().wrapper_max_exec_time))
    def xmlrpc_sync_remote_unlock_reboot(self, command_id, client, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.sync_remote_unlock_reboot(int(command_id), client, timeout or LauncherConfig().wrapper_max_exec_time))
    def xmlrpc_sync_remote_halt(self, command_id, client, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.sync_remote_halt(int(command_id), client, timeout or LauncherConfig().wrapper_max_exec_time))

    # Asynchrone commands summoning
    def xmlrpc_async_remote_push(self, command_id, client, files_list, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.async_remote_push(int(command_id), client, files_list, timeout or LauncherConfig().wrapper_max_exec_time))
    def xmlrpc_async_remote_pull(self, command_id, client, files_list, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.async_remote_pull(int(command_id), client, files_list, timeout or LauncherConfig().wrapper_max_exec_time))
    def xmlrpc_async_remote_delete(self, command_id, client, files_list, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.async_remote_delete(int(command_id), client, files_list, timeout or LauncherConfig().wrapper_max_exec_time))
    def xmlrpc_async_remote_exec(self, command_id, client, command, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.async_remote_exec(int(command_id), client, command, timeout or LauncherConfig().wrapper_max_exec_time))
    def xmlrpc_async_remote_quickaction(self, command_id, client, command, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.async_remote_quickaction(int(command_id), client, command, timeout or LauncherConfig().wrapper_max_exec_time))
    def xmlrpc_async_remote_inventory(self, command_id, client, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.async_remote_inventory(int(command_id), client, timeout or LauncherConfig().wrapper_max_exec_time))
    def xmlrpc_async_remote_reboot(self, command_id, client, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.async_remote_reboot(int(command_id), client, timeout or LauncherConfig().wrapper_max_exec_time))
    def xmlrpc_async_remote_lock_reboot(self, command_id, client, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.async_remote_lock_reboot(int(command_id), client, timeout or LauncherConfig().wrapper_max_exec_time))
    def xmlrpc_async_remote_unlock_reboot(self, command_id, client, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.async_remote_unlock_reboot(int(command_id), client, timeout or LauncherConfig().wrapper_max_exec_time))
    def xmlrpc_async_remote_halt(self, command_id, client, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.remote_exec.async_remote_halt(int(command_id), client, timeout or LauncherConfig().wrapper_max_exec_time))

    # Background process lists
    def xmlrpc_get_process_count(self):
        return xmlrpcCleanup(pulse2.launcher.process_control.getProcessCount())
    def xmlrpc_get_running_count(self):
        return xmlrpcCleanup(pulse2.launcher.process_control.getRunningCount())
    def xmlrpc_get_zombie_count(self):
        return xmlrpcCleanup(pulse2.launcher.process_control.getZombiesCount())
    def xmlrpc_get_process_ids(self):
        return xmlrpcCleanup(pulse2.launcher.process_control.getProcessIds())
    def xmlrpc_get_running_ids(self):
        return xmlrpcCleanup(pulse2.launcher.process_control.getRunningIds())
    def xmlrpc_get_zombie_ids(self):
        return xmlrpcCleanup(pulse2.launcher.process_control.getZombieIds())

    # Background process statistics
    def xmlrpc_get_process_stdout(self, command_id):
        return xmlrpcCleanup(pulse2.launcher.process_control.getProcessStdout(int(command_id)))
    def xmlrpc_get_process_stderr(self, command_id):
        return xmlrpcCleanup(pulse2.launcher.process_control.getProcessStderr(int(command_id)))
    def xmlrpc_get_process_exitcode(self, command_id):
        return xmlrpcCleanup(pulse2.launcher.process_control.getProcessExitcode(int(command_id)))
    def xmlrpc_get_process_times(self, command_id):
        return xmlrpcCleanup(pulse2.launcher.process_control.getProcessTimes(int(command_id)))
    def xmlrpc_get_process_state(self, command_id):
        return xmlrpcCleanup(pulse2.launcher.process_control.getProcessState(int(command_id)))
    def xmlrpc_get_process_statistics(self, command_id):
        return xmlrpcCleanup(pulse2.launcher.process_control.getProcessStatistics(int(command_id)))

    # Background process handling
    def xmlrpc_stop_process(self, command_id):
        return xmlrpcCleanup(pulse2.launcher.process_control.stopProcess(int(command_id)))
    def xmlrpc_cont_process(self, command_id):
        return xmlrpcCleanup(pulse2.launcher.process_control.contProcess(int(command_id)))
    def xmlrpc_int_process(self, command_id):
        return xmlrpcCleanup(pulse2.launcher.process_control.intProcess(int(command_id)))
    def xmlrpc_term_process(self, command_id):
        return xmlrpcCleanup(pulse2.launcher.process_control.termProcess(int(command_id)))
    def xmlrpc_term_processes(self, command_ids):
        for command_id in command_ids:
            pulse2.launcher.process_control.termProcess(int(command_id))
        return True
    def xmlrpc_kill_process(self, command_id):
        return xmlrpcCleanup(pulse2.launcher.process_control.killProcess(int(command_id)))
    def xmlrpc_hup_process(self, command_id):
        return xmlrpcCleanup(pulse2.launcher.process_control.hupProcess(int(command_id)))

    # Massive background process lists
    def xmlrpc_stop_all_process(self):
        return xmlrpcCleanup(pulse2.launcher.process_control.stopAllProcess())
    def xmlrpc_cont_all_process(self):
        return xmlrpcCleanup(pulse2.launcher.process_control.contAllProcess())
    def xmlrpc_int_all_process(self):
        return xmlrpcCleanup(pulse2.launcher.process_control.intAllProcess())
    def xmlrpc_term_all_process(self):
        return xmlrpcCleanup(pulse2.launcher.process_control.termAllProcess())
    def xmlrpc_kill_all_process(self):
        return xmlrpcCleanup(pulse2.launcher.process_control.killAllProcess())
    def xmlrpc_hup_all_process(self):
        return xmlrpcCleanup(pulse2.launcher.process_control.hupAllProcess())

    # Network stuff handling
    def xmlrpc_wol(self, mac_addrs, target_bcast = None):
        return xmlrpcCleanup(pulse2.launcher.network.wolClient(mac_addrs, target_bcast))
    def xmlrpc_icmp(self, client, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.network.icmpClient(client, timeout or LauncherConfig().max_ping_time))
    def xmlrpc_probe(self, client, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.network.probeClient(client, timeout or LauncherConfig().max_probe_time))

    # TCP SProxy stuff
    def xmlrpc_tcp_sproxy(self, target, requestor_ip, requested_port):
        return xmlrpcCleanup(pulse2.launcher.tcp_sproxy.establishProxy(target, requestor_ip, requested_port))

    def xmlrpc_download_file(self, client, path, bwlimit = 0, timeout = None):
        return xmlrpcCleanup(pulse2.launcher.network.downloadFile(client, path, bwlimit, timeout or LauncherConfig().wrapper_max_exec_time))

    def xmlrpc_get_pubkey(self, key_name):
        return xmlrpcCleanup(pulse2.launcher.utils.getPubKey(key_name))

    def xmlrpc_get_health(self):
        return xmlrpcCleanup(getHealth(self.config))

    def xmlrpc_get_balance(self):
        return xmlrpcCleanup(getBalance(self.config))

def main():
    parser = optparse.OptionParser()
    parser.add_option("-c", "--config-file", help='path to the config file',
                      default=mmcconfdir + '/pulse2/launchers/launchers.ini')
    parser.add_option("-i", "--id", help='id of this launcher')
    parser.add_option("-l", "--log-config-file", help="path to the logging config file",
                      default = mmcconfdir + "/pulse2/launchers/launchers.ini")
    (options, args) = parser.parse_args()

    if not os.path.exists(options.config_file):
        print "Config file '%s' does not exist." % options.config_file
        sys.exit(3)

    if not options.id:
        print "The launcher id should be specified on the command line with --id option"
        sys.exit(3)

    if not os.path.exists(options.log_config_file):
        print "Logging config file '%s' does not exist." % options.config_file
        sys.exit(3)

    # start logger
    logging.config.fileConfig(options.log_config_file)
    logger = logging.getLogger()
    logger.info('launcher %s: starting' % options.id)

    # parse conf
    logger.info("launcher %s: Reading configuration file: %s" % (options.id, options.config_file))
    try:
        LauncherConfig().setup(options.config_file, options.id)
        LauncherConfig().setup_post_permission()
        LauncherConfig().check()
    except Exception, e:
        logger.error(e)
        logger.error("Please fix the configuration file")
        sys.exit(1)

    # setup SSH Agent
    if LauncherConfig().is_sshagent_available:
        pulse2.launcher.ssh_agent.setupSSHAgent()

    # start service
    sys.exit(startService())

if __name__ == '__main__':
    main()
