#!/usr/bin/env python

"""Bcfg2 Client"""
__revision__ = '$Revision$'

import fcntl
import logging
import os
import signal
import socket
import stat
import sys
import tempfile
import time
import Bcfg2.Options
import Bcfg2.Client.XML
import Bcfg2.Client.Frame
import Bcfg2.Client.Tools
# Compatibility imports
from Bcfg2.Bcfg2Py3k import xmlrpclib

import Bcfg2.Proxy
import Bcfg2.Logger

logger = logging.getLogger('bcfg2')

def cb_sigint_handler(signum, frame):
    """Exit upon CTRL-C."""
    os._exit(1)

DECISION_LIST = Bcfg2.Options.Option('Decision List', default=False,
                                     cmd="--decision-list", odesc='<file>',
                                     long_arg=True)


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': Bcfg2.Options.VERBOSE,
            'extra': Bcfg2.Options.CLIENT_EXTRA_DISPLAY,
            'quick': Bcfg2.Options.CLIENT_QUICK,
            'debug': Bcfg2.Options.DEBUG,
            'lockfile': Bcfg2.Options.LOCKFILE,
            'drivers': Bcfg2.Options.CLIENT_DRIVERS,
            'dryrun': Bcfg2.Options.CLIENT_DRYRUN,
            'paranoid': Bcfg2.Options.CLIENT_PARANOID,
            'bundle': Bcfg2.Options.CLIENT_BUNDLE,
            'bundle-quick': Bcfg2.Options.CLIENT_BUNDLEQUICK,
            'indep': Bcfg2.Options.CLIENT_INDEP,
            'file': Bcfg2.Options.CLIENT_FILE,
            'interactive': Bcfg2.Options.INTERACTIVE,
            'cache': Bcfg2.Options.CLIENT_CACHE,
            'profile': Bcfg2.Options.CLIENT_PROFILE,
            'remove': Bcfg2.Options.CLIENT_REMOVE,
            'help': Bcfg2.Options.HELP,
            'setup': Bcfg2.Options.CFILE,
            'server': Bcfg2.Options.SERVER_LOCATION,
            'user': Bcfg2.Options.CLIENT_USER,
            'password': Bcfg2.Options.SERVER_PASSWORD,
            'retries': Bcfg2.Options.CLIENT_RETRIES,
            'kevlar': Bcfg2.Options.CLIENT_KEVLAR,
            'decision-list': DECISION_LIST,
            'encoding': Bcfg2.Options.ENCODING,
            'omit-lock-check': Bcfg2.Options.OMIT_LOCK_CHECK,
            'filelog': Bcfg2.Options.LOGGING_FILE_PATH,
            'decision': Bcfg2.Options.CLIENT_DLIST,
            'servicemode': Bcfg2.Options.CLIENT_SERVICE_MODE,
            'key': Bcfg2.Options.CLIENT_KEY,
            'certificate': Bcfg2.Options.CLIENT_CERT,
            'ca': Bcfg2.Options.CLIENT_CA,
            'serverCN': Bcfg2.Options.CLIENT_SCNS,
            'timeout': Bcfg2.Options.CLIENT_TIMEOUT,
            }

        self.setup = Bcfg2.Options.OptionParser(optinfo)
        self.setup.parse(sys.argv[1:])

        if self.setup['args']:
            print("Bcfg2 takes no arguments, only options")
            print(self.setup.buildHelpMessage())
            raise SystemExit(1)
        level = 30
        if self.setup['verbose']:
            level = 20
        if self.setup['debug']:
            level = 0
        Bcfg2.Logger.setup_logging('bcfg2',
                                   to_syslog=False,
                                   level=level,
                                   to_file=self.setup['filelog'])
        self.logger = logging.getLogger('bcfg2')
        self.logger.debug(self.setup)
        if self.setup['bundle-quick']:
            if self.setup['bundle'] == []:
                self.logger.error("-Q option requires -b")
                raise SystemExit(1)
            elif self.setup['remove'] != False:
                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']:
            self.logger.error("Service removal is nonsensical, disable services to get former behavior")
        if self.setup['remove'] not in [False,
                                        'all',
                                        'Services',
                                        'Packages',
                                        '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 not self.setup['server'].startswith('https://'):
            self.setup['server'] = 'https://' + self.setup['server']

    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 = open(scriptname, 'w+')
            try:
                script.write("#!%s\n" %
                             (probe.attrib.get('interpreter', '/bin/sh')))
                script.write(probe.text)
                script.close()
                os.close(scripthandle)
                os.chmod(script.name, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH |
                                      stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH |
                                      stat.S_IWUSR)  # 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))
        os._exit(1)

    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
            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'])

            if self.setup['profile']:
                try:
                    proxy.AssertProfile(self.setup['profile'])
                except Bcfg2.Proxy.ProxyError:
                    err = sys.exc_info()[1]
                    self.fatal_error("Failed to set client profile")
                    self.logger.error(str(err))
                    raise SystemExit(1)

            try:
                probe_data = proxy.GetProbes()
            except (Bcfg2.Proxy.ProxyError,
                    Bcfg2.Proxy.CertificateError,
                    socket.gaierror,
                    socket.error):
                err = sys.exc_info()[1]
                self.logger.error("Failed to download probes from bcfg2: %s" %
                                  err)
                raise SystemExit(1)

            times['probe_download'] = time.time()

            try:
                probes = Bcfg2.Client.XML.XML(probe_data)
            except Bcfg2.Client.XML.ParseError:
                syntax_error = sys.exc_info()[1]
                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,
                                                                  encoding='UTF-8',
                                                                  xml_declaration=True))
                except Bcfg2.Proxy.ProxyError:
                    err = sys.exc_info()[1]
                    self.logger.error("Failed to upload probe data: %s" % err)
                    raise SystemExit(1)

            times['probe_upload'] = time.time()

            if self.setup['decision'] in ['whitelist', 'blacklist']:
                try:
                    self.setup['decision_list'] = \
                        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.logger.error("Failed to get decision list: %s" % err)
                    raise SystemExit(1)

            try:
                rawconfig = proxy.GetConfig().encode('UTF-8')
            except Bcfg2.Proxy.ProxyError:
                err = sys.exc_info()[1]
                self.logger.error("Failed to download configuration from "
                                  "Bcfg2: %s" % err)
                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 = 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/>')
            [newconfig.append(bundle) for bundle in self.config.getchildren() if \
             bundle.tag == 'Bundle' and bundle.get('name') in self.setup['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:
                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:
                proxy.RecvStats(Bcfg2.Client.XML.tostring(feedback,
                                                          encoding='UTF-8',
                                                          xml_declaration=True))
            except Bcfg2.Proxy.ProxyError:
                err = sys.exc_info()[1]
                self.logger.error("Failed to upload configuration statistics: "
                                  "%s" % err)
                raise SystemExit(2)

if __name__ == '__main__':
    signal.signal(signal.SIGINT, cb_sigint_handler)
    client = Client()
    spid = os.getpid()
    client.run()
