extras-buildsys/client client.py,1.26.2.1,1.26.2.2

Daniel Williams (dcbw) fedora-extras-commits at redhat.com
Wed Aug 24 19:23:22 UTC 2005


Author: dcbw

Update of /cvs/fedora/extras-buildsys/client
In directory cvs-int.fedora.redhat.com:/tmp/cvs-serv31499/client

Modified Files:
      Tag: STABLE_0_3
	client.py 
Log Message:
2005-08-24  Dan Williams <dcbw at redhat.com>

    * common/BaseConfig.py
      client/client.py
        - Rewrite the client to make the code less stupid,
            more easily followed, and to validate more
            input options
        - Also fixes RH #166692




Index: client.py
===================================================================
RCS file: /cvs/fedora/extras-buildsys/client/client.py,v
retrieving revision 1.26.2.1
retrieving revision 1.26.2.2
diff -u -r1.26.2.1 -r1.26.2.2
--- client.py	15 Aug 2005 14:33:05 -0000	1.26.2.1
+++ client.py	24 Aug 2005 19:23:20 -0000	1.26.2.2
@@ -18,6 +18,7 @@
 
 import sys, os
 from plague import XMLRPCServerProxy
+from plague import BaseConfig
 import ConfigParser
 import socket
 import xmlrpclib
@@ -27,22 +28,29 @@
 XMLRPC_API_VERSION = 1
 
 
-config = ConfigParser.ConfigParser()
-config.add_section('Certs')
-config.set('Certs', 'user-cert', '~/.fedora.cert')
-config.set('Certs', 'user-ca-cert', '~/.fedora-upload-ca.cert')
-config.set('Certs', 'server-ca-cert', '~/.fedora-server-ca.cert')
-
-config.add_section('Server')
-config.set('Server', 'use_ssl', 'True')
-config.set('Server', 'address', 'https://127.0.0.1:8887')
-
-config.add_section('User')
-config.set('User', 'email', 'foo at it.com')
+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.add_section("User")
+        self.set_option("User", "email", "foo at it.com")
 
-config_file_path = os.path.expanduser('~/.plague-client.cfg')
-config.read([config_file_path])
+        self.save()
 
 
 class ServerException:
@@ -53,181 +61,215 @@
     def __init__(self, message):
         self.message = message
 
-
-def check_api_version(server):
-    """ Ensure the API of the server matches the one we expect to talk to """
-
+def validate_jobid(jobid_in):
     try:
-        server_ver = server.api_version()
-    except socket.error, e:
-        print "Error connecting to build server: '%s'" % e
-        return False
-    except OpenSSL.SSL.SysCallError, e:
-        print "Error connecting to build server: '%s'" % e
-        return False
-    except xmlrpclib.Fault, fault:
-        print "Error: build server does not support 'api_version' method.  Server said: '%s'" % fault
-        return False
-
-    if server_ver != XMLRPC_API_VERSION:
-        print "Error: API version mismatch.  Client: %d, Server: %d" % (XMLRPC_API_VERSION, server_ver)
-        return False
+        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()
 
-    return True
+        # 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)
+        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 enqueue(server, email, args):
-    """ Enqueue a package on the server by CVS tag """
-
-    if len(args) != 3:
-        raise CommandException("Invalid command.  The 'enqueue' command only takes 3 arguments.")
-    package = args[0]
-    tag = args[1]
-    target = args[2]
-    use_ssl = config.get('Server', 'use_ssl')
-    if use_ssl.lower() == 'true':
-        (e, msg, uid) = server.enqueue(package, tag, target)
-    else:
-        (e, msg, uid) = server.enqueue(email, package, tag, target)
-    if e == -1:
-        print "Server returned an error: %s" % msg
-        return
-
-    if uid != -1:
-        print "Package %s enqueued.  Job ID: %d." % (package, uid)
-    else:
-        print "Package %s enqueued.  (However, no Job ID was provided in the time required)" % package
-
-
-def enqueue_srpm(server, email, args):
-    """ Enqueue a package on the server by SRPM """
-
-    if len(args) != 3:
-        raise CommandException("Invalid command.  The 'enqueue_srpm' command only takes 3 arguments.")
-    package = args[0]
-    srpm = args[1]
-    target = args[2]
-    use_ssl = config.get('Server', 'use_ssl')
-    if use_ssl.lower() == 'true':
-        (e, msg, uid) = server.enqueue_srpm(package, srpm, target)
-    else:
-        (e, msg, uid) = server.enqueue_srpm(email, package, srpm, target)
-    if e == -1:
-        print "Server returned an error: %s" % msg
-        return
-
-    if uid != -1:
-        print "Package %s enqueued.  Job ID: %d." % (package, uid)
-    else:
-        print "Package %s enqueued.  (However, no Job ID was provided in the time required)" % package
+    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(8192)
+            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) != 3:
+            raise CommandException("Invalid command.  The 'build' command takes 3 arguments.")
+        package = args[0]
+        source = args[1]
+        target = 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
+        self._enqueue(is_srpm, package, source, target)
+
+    def _enqueue(self, is_srpm, package, source, target):
+        """ Enqueue a package on the server by CVS tag """
+        use_ssl = self._cfg.get_bool("Server", "use_ssl")
+        if is_srpm:
+            if use_ssl:
+                (e, msg, jobid) = self._server.enqueue_srpm(package, source, target)
+            else:
+                (e, msg, jobid) = self._server.enqueue_srpm(self._email, package, source, target)
+        else:
+            if use_ssl:
+                (e, msg, jobid) = self._server.enqueue(package, source, target)
+            else:
+                (e, msg, jobid) = self._server.enqueue(self._email, package, source, target)
 
+        if e == -1:
+            print "Server returned an error: %s" % msg
+            return
 
-def requeue_job(server, email, uid):
-    try:
-        (e, msg) = server.requeue(uid)
-    except socket.error, e:
-        print "Error connecting to build server: '%s'" % e
-        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
 
-    print msg
+    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 validate_arg(arg):
-    args = ['email', 'status', 'result', 'uid', 'uid_gt', 'uid_lt']
-    if arg in args:
-        return True
-    return False
-
-def list_jobs(server, args):
-    """
-    List jobs by criteria
-    """
-
-    # Have to have an even number of options
-    if int(len(args) / 2.0) != (len(args) / 2.0):
-        print "Error: invalid options."
-        return
-
-    query_args = {}
-    cmd = ''
-    for arg in args:
-        if not len(cmd):
-            if validate_arg(arg):
-                cmd = arg
+    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:
-                print "Error: invalid option '%s'" % arg
-                return
+                # '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:
-            if cmd == 'status':
-                arg = [arg]
-            query_args[cmd] = arg
-            cmd = ''
-
-    if len(query_args) == 0:
-        # List all jobs
-        query_args['uid_gt'] = "0"
+            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']
+        print "Target: %s" % jobrec['target']
+        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'])
 
-    try:
-        (e, msg, jobs) = server.list_jobs(query_args)
-    except socket.error, e:
-        print "Error connecting to build server: '%s'" % e
-        return
+        print ""
 
-    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 detail_job(server, jobid):
-    """
-    Get a single job's details
-    """
-    try:
-        (err, msg, jobrec) = server.detail_job(jobid)
-    except socket.error, e:
-        print "Error connecting to build server: '%s'" % e
-        return
-    if err == -1:
+    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
-        return
-
-    print "\nDetail for Job ID %d (%s):" % (int(jobrec['uid']), jobrec['package'])
-    print "-" * 80
-    print "Source: %s" % jobrec['source']
-    print "Target: %s" % jobrec['target']
-    print "Submitter: %s" % jobrec['username']
-    print "Status: %s/%s" % (jobrec['status'], jobrec['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 kill(server, email, jobid):
-    """
-    Kill a job on the build server.
-    """
-
-    try:
-        (err, msg) = server.kill_job(email, jobid)
-    except socket.error, e:
-        print "Error connecting to build server: '%s'" % e
-        return
-
-    print msg
 
+    def _print_builders(self, builder_list):
+        if len(builder_list) == 0:
+            print "No builders found."
+            return
 
-def print_builders(builder_list):
         print "\nBuilders:"
         print "-" * 90
         for builder in builder_list:
@@ -240,132 +282,54 @@
             print string
         print ""
 
+    def _cmd_update_builders(self, args):
+        (e, msg, builder_list) = self._server.update_builders()
+        self._print_builders(builder_list)
+
+    def _cmd_list_builders(self, args):
+        (e, msg, builder_list) = self._server.list_builders()
+        self._print_builders(builder_list)
 
-def update_builders(server, email):
-    """
-    Tell the build server to requery its builder list ASAP.
-    """
-
-    try:
-        (e, msg, builder_list) = server.update_builders()
-        if len(builder_list) > 0:
-            print_builders(builder_list)
-        else:
-            print "No builders found."
-    except socket.error, e:
-        print "Error connecting to build server: '%s'" % e
-
-
-def list_builders(server, email):
-    """
-    Get a list of currently active builders.
-    """
-
-    try:
-        (e, msg, builder_list) = server.list_builders()
-        if len(builder_list) > 0:
-            print_builders(builder_list)
-        else:
-            print "No builders found."
-    except socket.error, e:
-        print "Error connecting to build server: '%s'" % e
-
-def pause(server, paused):
-    """
-    Pause or unpause the build server
-    """
-
-    try:
+    def _do_pause(self, paused):
         (e, msg) = server.pause(paused)
         print msg
-    except socket.error, e:
-        print "Error connecting to build server: '%s'" % e
 
+    def _cmd_pause(self, args):
+        self._do_pause(True)
 
-def is_paused(server):
-    """
-    Pause or unpause the build server
-    """
+    def _cmd_unpause(self, args):
+        self._do_pause(False)
 
-    try:
-        if server.is_paused():
+    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."
-    except socket.error, e:
-        print "Error connecting to build server: '%s'" % e
+            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)
 
-def finish(server, args):
-    uid_list = []
-    for uid in args:
-        uid_list.append(int(uid))
-
-    try:
-        (e, msg) = server.finish(uid_list)
-    except socket.error, e:
-        print "Error connecting to build server: '%s'" % e
-
-    print msg
+        (e, msg) = self._server.finish(jobid_list)
+        print msg
 
-def getXMLRPCServerProxy(use_ssl):
-    """
-    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 = config.get('Server', 'address')
-    use_ssl = use_ssl.lower()
-    if use_ssl == 'true':
-        if addr.startswith("http:"):
-            raise ServerException("Error: '%s' is not an SSL server, but the use_ssl config option set to True.  See %s" % (addr, config_file_path))
-        else:
-            certs = {}
-            certs['key_and_cert'] = os.path.expanduser(config.get('Certs', 'user-cert'))
-            certs['ca_cert'] = os.path.expanduser(config.get('Certs', 'user-ca-cert'))
-            certs['peer_ca_cert'] = os.path.expanduser(config.get('Certs', 'server-ca-cert'))
-            server = XMLRPCServerProxy.PlgXMLRPCServerProxy(addr, certs)
-    elif use_ssl == 'false':
-        if addr.startswith("https:"):
-            raise ServerException("Error: '%s' is an SSL server, but the use_ssl config option set to False.  See %s" % (addr, config_file_path))
-        else:
-            server = xmlrpclib.ServerProxy(addr)
-    else:
-        raise ServerException("Unrecognized value for config '%s' option use_ssl.  See %s" % (use_ssl, config_file_path))
-
-    return server
-
-
-def getEmailAddress():
-    """ Get email address either from certificate of config file """
-    config_email = config.get('User', 'email')
-
-    use_ssl = config.get('Server', 'use_ssl')
-    if use_ssl.lower() == 'true':
-        certfile = config.get('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(8192)
-        f.close()
-        cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, buf)
-        cert_email = cert.get_subject().emailAddress
-        if cert_email != config_email:
-            print "Error: certificate's email address does not match the email address in the config file."
-            sys.exit(1)
+    def dispatch(self, cmd, args):
+        try:
+            func = getattr(self, "_cmd_%s" % cmd)
+            func(args)
+        except AttributeError:
+            raise CommandException("Unknown command '%s'." % cmd)
 
-    return config_email
 
 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"
-    print "      list <status>"
-    print "      list <email> <status>"
+    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"
@@ -383,67 +347,41 @@
         Usage()
         sys.exit(1)
 
-    # Write out config file if it doesn't exist
-    if not os.path.exists(config_file_path):
-        f = open(config_file_path, "w+")
-        config.write(f)
-        f.close()
-
-    cmd = sys.argv[1]
-    email = getEmailAddress()
+    # 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 PLAUGE_CLIENT_CONFIG" \
+                    " environment variable (%s) did not exist." % cfg_file
+            sys.exit(1)
+    except KeyError:
+        pass
 
     try:
-        server = getXMLRPCServerProxy(config.get('Server', 'use_ssl'))
+        cli = PlagueClient(os.path.expanduser(cfg_file))
     except ServerException, e:
         print e.message
         sys.exit(1)
-
-    # Ensure the server's API version matches ours
-    if not check_api_version(server):
+    except BaseConfig.ConfigError, e:
+        print e
         sys.exit(1)
 
-    if cmd == 'build':
-        if len(sys.argv) < 5:
+    try:
+        cmd = sys.argv[1]
+        if cmd == 'help':
             Usage()
-            sys.exit(1)
-        item = sys.argv[3]
-        try:
-            if item.endswith(".src.rpm") and os.path.exists(item):
-                enqueue_srpm(server, email, sys.argv[2:])
-            else:
-                enqueue(server, email, sys.argv[2:])
-        except CommandException, e:
-            print e.message
-    elif cmd == 'requeue':
-        jobid = sys.argv[2]
-        requeue_job(server, email, jobid)
-    elif cmd == 'list':
-        list_jobs(server, sys.argv[2:])
-    elif cmd == 'detail':
-        detail_job(server, sys.argv[2])
-    elif cmd == 'kill':
-        if len(sys.argv) < 3:
-            print "Error: need a job UID to kill"
-            sys.exit(1)
-        jobid = sys.argv[2]
-        kill(server, email, jobid)
-    elif cmd == 'update_builders':
-        update_builders(server, email)
-    elif cmd == 'list_builders':
-        list_builders(server, email)
-    elif cmd == 'pause':
-        pause(server, True)
-    elif cmd == 'unpause':
-        pause(server, False)
-    elif cmd == 'is_paused':
-        is_paused(server)
-    elif cmd == 'finish':
-        finish(server, sys.argv[2:])
-    elif cmd == 'help':
+        else:
+            cli.dispatch(cmd, sys.argv[2:])
+    except CommandException, e:
+        print e.message + "\n"
         Usage()
         sys.exit(1)
-    else:
-        print "Unknown command."
-        Usage()
+    except socket.error, e:
+        print "Error connecting to build server: '%s'" % e
         sys.exit(1)
 
+    sys.exit(0)
+




More information about the fedora-extras-commits mailing list