#!/usr/bin/python
#
# This program 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.
#
# This program 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 Library General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# Copyright 2005 Dan Williams <dcbw@redhat.com> and Red Hat, Inc.

import sys, os
from plague import XMLRPCServerProxy
from plague import BaseConfig
import ConfigParser
import socket
import xmlrpclib
import OpenSSL


XMLRPC_API_VERSION = 100


class ClientConfig(BaseConfig.BaseConfig):
    def __init__(self, filename):
        BaseConfig.BaseConfig.__init__(self, filename)
        try:
            self.open()
        except BaseConfig.ConfigError, e:
            print "Config file did not exist.  Writing %s with default values.  Error: %s" % (filename, e)
            self.save_default_config()

    def save_default_config(self, filename=None):
        self.add_section("Certs")
        self.set_option("Certs", "user-cert", "~/.fedora.cert")
        self.set_option("Certs", "user-ca-cert", "~/.fedora-upload-ca.cert")
        self.set_option("Certs", "server-ca-cert", "~/.fedora-server-ca.cert")

        self.add_section("Server")
        self.set_option("Server", "use_ssl", "yes")
        self.set_option("Server", "address", "https://127.0.0.1:8887")
        self.set_option("Server", "allow_uploads", "no")
        self.set_option("Server", "upload_user", "me")

        self.add_section("User")
        self.set_option("User", "email", "foo@it.com")

        self.save()


class ServerException:
    def __init__(self, message):
        self.message = message

class CommandException:
    def __init__(self, message):
        self.message = message

def validate_jobid(jobid_in):
    try:
        jobid = int(jobid_in)
    except ValueError:
        raise CommandException("Invalid jobid.")
    except TypeError:
        raise CommandException("Invalid jobid.")
    if jobid < 0:
        raise CommandException("Invalid jobid.")
    return jobid

class PlagueClient:
    def __init__(self, cfg_file):
        self._cfg_file = cfg_file
        self._cfg = ClientConfig(cfg_file)
        self._email = self._get_user_email()
        self._server = self._get_xmlrpc_server_proxy()

        # Ensure the server's API version matches ours
        self._check_api_version(self._server)

    def _check_api_version(self, server):
        """ Ensure the API of the server matches the one we expect to talk to """
        try:
            server_ver = server.api_version()
        except socket.error, e:
            raise ServerException("Error connecting to build server: '%s'" % e)
        except OpenSSL.SSL.SysCallError, e:
            raise ServerException("Error connecting to build server: '%s'" % e)
        except xmlrpclib.Fault, fault:
            raise ServerException("Error: build server does not support 'api_version' method.  Server said: '%s'" % fault)

        if server_ver != XMLRPC_API_VERSION:
            raise ServerException("Error: API version mismatch.  Client: %d, Server: %d" % (XMLRPC_API_VERSION, server_ver))

    def _get_xmlrpc_server_proxy(self):
        """
        Return an XMLRPC server proxy object, either one that uses SSL with certificates
        for verification, or one that doesn't do any authentication/encryption at all.
        """
        server = None
        addr = self._cfg.get_str("Server", "address")
        if self._cfg.get_bool("Server", "use_ssl"):
            if addr.startswith("http:"):
                raise ServerException("Error: '%s' is not an SSL server, but the use_ssl " \
                            " config option set to 'yes'.  Fix %s" % (addr, self._cfg_file))
            else:
                certs = {}
                certs['key_and_cert'] = os.path.expanduser(self._cfg.get_str('Certs', 'user-cert'))
                certs['ca_cert'] = os.path.expanduser(self._cfg.get_str('Certs', 'user-ca-cert'))
                certs['peer_ca_cert'] = os.path.expanduser(self._cfg.get_str('Certs', 'server-ca-cert'))
                server = XMLRPCServerProxy.PlgXMLRPCServerProxy(addr, certs, timeout=20)
        else:
            if addr.startswith("https:"):
                raise ServerException("Error: '%s' is an SSL server, but the use_ssl " \
                            "config option set to 'no'.  Fix %s" % (addr, self._cfg_file))
            else:
                server = xmlrpclib.ServerProxy(addr)
        return server

    def _get_user_email(self):
        """ Get email address either from certificate of config file """
        cfg_email = self._cfg.get_str("User", "email")
        if self._cfg.get_bool('Server', 'use_ssl'):
            certfile = self._cfg.get_str('Certs', 'user-cert')
            certfile = os.path.expanduser(certfile)
            if not os.access(certfile, os.R_OK):
                print "%s does not exist or is not readable." % certfile
                sys.exit(1)
            f = open(certfile, "r")
            buf = f.read()
            f.close()
            cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, buf)
            cert_email = cert.get_subject().emailAddress
            if cert_email != cfg_email:
                print "Error: certificate's email address (%s) does not match the " \
                        "email address in %s (%s)." % (cert_email, self._cfg_file, cfg_email)
                sys.exit(1)
        return cfg_email

    def _cmd_build(self, args):
        # Be smart about local SRPMs getting enqueued
        if len(args) >= 2 and args[0].find('/') != -1 and os.path.exists(args[0]):
            # We were given an RPM, find the package name
            import rpmUtils.transaction
            ts = rpmUtils.transaction.initReadOnlyTransaction()
            hdr = rpmUtils.miscutils.hdrFromPackage(ts, args[0])
            package = hdr['name']
            source = args[0]
            target_alias = args[1]
            del hdr
            del ts
        elif len(args) == 3:
            package = args[0]
            source = args[1]
            target_alias = args[2]
        else:
            raise CommandException("Invalid number of arguments.")

        is_srpm = False
        if source.endswith(".src.rpm"):
            if not os.path.exists(source):
                raise CommandException("The SRPM %s does not exist." % source)
            is_srpm = True
            source = os.path.abspath(os.path.expanduser(source))
        self._enqueue(is_srpm, package, source, target_alias)

    def _upload_srpm(self, host, source, target_alias):
        # Get the package upload directory from the server
        (err, path) = self._server.srpm_upload_dir(target_alias)
        if err != 0:
            print "Error: Could not upload package %s.  " \
                    "reason: couldn't get upload dir for %s" % (source, target_alias)
            return (-1, source)

        upload_file = os.path.join(path, os.path.basename(source))
        user = None
        if self._cfg.has_option("Server", "upload_user"):
            user = self._cfg.get_str("Server", "upload_user")
        else:
            import pwd
            user = pwd.getpwuid(os.getuid())[0]
        cmd = "/usr/bin/scp %s %s@%s:%s" % (source, user, host, upload_file)
        print "Executing: %s" % cmd
        os.system(cmd)
        return (0, upload_file)

    def _enqueue(self, is_srpm, package, source, target_alias):
        """ Enqueue a package on the server """

        allow_up = False
        try:
            allow_up = self._cfg.get_bool("Server", "allow_uploads")
        except BaseConfig.ConfigError, e:
            pass
    
        if allow_up:
            import urllib
            addr = self._cfg.get_str("Server", "address")
            if addr.startswith("http") or addr.startswith("https"):
                idx = addr.find('//')
                addr = addr[idx:]
            host_port, path = urllib.splithost(addr)
            host, port = urllib.splitport(host_port)
            # Don't upload if we're going to scp to the same machine we're on
            if is_srpm and socket.gethostname() != host:
                (err, source) = self._upload_srpm(host, source, target_alias)
                if err == -1:
                    sys.exit(1)

        use_ssl = self._cfg.get_bool("Server", "use_ssl")
        if use_ssl:
            (e, msg, jobid) = self._server.enqueue(package, source, target_alias)
        else:
            (e, msg, jobid) = self._server.enqueue(self._email, package, source, target_alias)

        if e == -1:
            print "Server returned an error: %s" % msg
            return

        if jobid != -1:
            print "Package %s enqueued.  Job ID: %d." % (package, jobid)
        else:
            print "Package %s enqueued.  (However, no Job ID was provided in the time required)" % package

    def _cmd_requeue(self, args):
        if len(args) != 1:
            raise CommandException("Invalid options.")
        jobid = validate_jobid(args[0])
        (e, msg) = self._server.requeue(jobid)
        print msg

    def _validate_list_opt(self, arg):
        args = ['email', 'status', 'result', 'uid', 'uid_gt', 'uid_lt']
        if arg in args:
            return True
        return False

    def _cmd_list(self, args):
        # Have to have an even number of options
        if int(len(args) / 2.0) != (len(args) / 2.0):
            raise CommandException("Invalid number of arguments.")

        query_args = {}
        cmd = ''
        for arg in args:
            if not len(cmd):
                if self._validate_list_opt(arg):
                    cmd = arg
                else:
                    raise CommandException("Error: invalid option '%s'" % arg)
            else:
                # 'status' call takes a sequence
                if cmd == 'status':
                    arg = [arg]
                # Validate options that take a jobid
                if cmd == 'uid' or cmd == 'uid_lt' or cmd == 'uid_gt':
                    temp = validate_jobid(arg)
                query_args[cmd] = arg
                cmd = ''

        if len(query_args) == 0:
            # List all jobs
            query_args['uid_gt'] = "0"

        (e, msg, jobs) = self._server.list_jobs(query_args)
        if e == -1:
            print msg
        elif len(jobs) == 0:
            print "No jobs found that match the search criteria."
        else:
            for job in jobs:
                try:
                    print "%d: %s (%s)  %s   %s/%s" % (job['uid'], job['package'], job['source'], job['username'], job['status'], job['result'])    
                    for archjob in job['archjobs']:
                        print "\t%s(%s): %s %s/%s" % (archjob['builder_addr'], archjob['arch'], archjob['jobid'], archjob['status'], archjob['builder_status'])
                    print ''
                except IOError:
                    pass

    def _cmd_detail(self, args):
        if len(args) != 1:
            raise CommandException("Invalid options.")
        jobid = validate_jobid(args[0])
        (e, msg, jobrec) = self._server.detail_job(jobid)
        if e == -1:
            print msg
            return

        print "\nDetail for Job ID %d (%s):" % (int(jobrec['uid']), jobrec['package'])
        print "-" * 80
        print "Source: %s" % jobrec['source']
        target_string = "%s-%s-%s" % (jobrec['target_distro'], jobrec['target_target'], jobrec['target_repo'])
        print "Target: %s" % target_string
        print "Submitter: %s" % jobrec['username']
        try:
            result = jobrec['result']
        except KeyError:
            result = ''
        print "Status: %s/%s" % (jobrec['status'], result)

        print "Archjobs:"
        for aj in jobrec['archjobs']:
            print "    %s: %s    %s/%s" % (aj['arch'], aj['builder_addr'], aj['status'], aj['builder_status'])

        print ""

    def _cmd_kill(self, args):
        if len(args) != 1:
            raise CommandException("Invalid options.")
        jobid = validate_jobid(args[0])
        (e, msg) = self._server.kill_job(self._email, jobid)
        print msg

    def _print_builders(self, builder_list):
        if len(builder_list) == 0:
            print "No builders found."
            return

        print "\nBuilders:"
        print "-" * 90
        for builder in builder_list:
            builder_addr = builder['address']
            string = "  " + builder_addr
            string = string + " " * (40 - len(builder_addr))
            for arch in builder['arches']:
                string = string + arch + " "
            alive = 'unavailable'
            if builder['alive']:
                alive = 'alive'
            string = string + "    " + alive
            print string
        print ""

    def _cmd_update_builders(self, args):
        (e, msg, builder_list) = self._server.update_builders()
        if e==0:
            self._print_builders(builder_list)
        else:
            print msg

    def _cmd_list_builders(self, args):
        (e, msg, builder_list) = self._server.list_builders()
        self._print_builders(builder_list)

    def _do_pause(self, paused):
        (e, msg) = self._server.pause(paused)
        print msg

    def _cmd_pause(self, args):
        self._do_pause(True)

    def _cmd_unpause(self, args):
        self._do_pause(False)

    def _cmd_is_paused(self, args):
        if self._server.is_paused():
            print "The build server is paused."
        else:
            print "The build server is not paused."

    def _cmd_finish(self, args):
        if len(args) == 0:
            raise CommandException("Invalid options.")
        jobid_list = []
        for jobid in args:
            int_jobid = validate_jobid(jobid)
            jobid_list.append(int_jobid)

        (e, msg) = self._server.finish(jobid_list)
        print msg

    def dispatch(self, cmd, args):
        try:
            func = getattr(self, "_cmd_%s" % cmd)
            func(args)
        except AttributeError:
            raise CommandException("Unknown command '%s'." % cmd)


def Usage():
    print "Usage:\nplague-client.py <command>\n"
    print "      <command> is one of:"
    print "      build <package_name> <cvs_tag | srpm_path> <target>"
    print "      list [email <addr>] [status <stat>] [result <result>] [uid <uid>]"
    print "      kill <jobid>"
    print "      update_builders"
    print "      list_builders"
    print "      pause"
    print "      unpause"
    print "      is_paused"
    print "      requeue <jobid>"
    print "      detail <jobid>"
    print "      finish <jobid>"
    print "      help"
    print ""

if __name__ == '__main__':
    if len(sys.argv) < 2:
        Usage()
        sys.exit(1)

    # Figure out the path of the config file.  If the
    # PLAGUE_CLIENT_CONFIG environment variable is set, use
    # that.  Otherwise, use ~/.plague-client.cfg
    cfg_file = "~/.plague-client.cfg"
    try:
        cfg_file = os.environ['PLAGUE_CLIENT_CONFIG']
        if not os.path.exists(cfg_file):
            print "Config file specified in PLAGUE_CLIENT_CONFIG" \
                    " environment variable (%s) did not exist." % cfg_file
            sys.exit(1)
    except KeyError:
        pass

    try:
        cli = PlagueClient(os.path.expanduser(cfg_file))
    except ServerException, e:
        print e.message
        sys.exit(1)
    except BaseConfig.ConfigError, e:
        print e
        sys.exit(1)

    exit_val = 1
    try:
        cmd = sys.argv[1]
        if cmd == 'help':
            Usage()
        else:
            cli.dispatch(cmd, sys.argv[2:])
        exit_val = 0
    except CommandException, e:
        print e.message + "\n"
        Usage()
    except socket.timeout, e:
        print "Error: connection to the server timed out. '%s'" % e
    except (socket.error, OpenSSL.SSL.SysCallError), e:
        print "Error: an error ocurred connecting to the server. '%s'" % e
    except xmlrpclib.Fault, e:
        print "Error: an error ocurred communicating with the server. '%s'" % e

    sys.exit(exit_val)

