#!/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 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(20000) 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): if len(args) != 2 and len(args) != 3: raise CommandException("Invalid command. The 'build' command takes 3 arguments.") # Be smart about local SRPMs getting enqueued if args[0].find('/') != -1 and os.path.exists(args[0]): # We were given an RPM, find the package name import rpmUtils 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 else: package = args[0] source = args[1] target_alias = args[2] 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['available']: alive = 'available' 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 \n" print " is one of:" print " build " print " list [email ] [status ] [result ] [uid ]" print " kill " print " update_builders" print " list_builders" print " pause" print " unpause" print " is_paused" print " requeue " print " detail " print " finish " 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)