[PATCH] Better support for koji profiles.

Mike McLean mikem at redhat.com
Wed Feb 4 10:55:45 UTC 2015


On 02/03/2015 09:32 AM, Daniel Mach wrote:
> Create python modules for each koji instance,
> so it's easier to work with them in python code.
> Also move profile related code from CLI to koji library.
> See docs/profiles for more details.

At a glance this looks reasonable, but I need to take a little more time
to review since it conflicts with another patch.

> ---
>  cli/koji         |  82 ++-------------------------------------
>  docs/profiles    |  50 ++++++++++++++++++++++++
>  koji/__init__.py | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-
>  3 files changed, 166 insertions(+), 80 deletions(-)
>  create mode 100644 docs/profiles
> 
> diff --git a/cli/koji b/cli/koji
> index 6eba62b..28533e1 100755
> --- a/cli/koji
> +++ b/cli/koji
> @@ -202,84 +202,10 @@ def get_options():
>          list_commands()
>          parser.error('Unknown command: %s' % args[0])
>          assert False
> -    # load local config
> -    defaults = {
> -        'server' : 'http://localhost/kojihub',
> -        'weburl' : 'http://localhost/koji',
> -        'topurl' : None,
> -        'pkgurl' : None,
> -        'topdir' : '/mnt/koji',
> -        'max_retries' : None,
> -        'retry_interval': None,
> -        'anon_retry' : None,
> -        'offline_retry' : None,
> -        'offline_retry_interval' : None,
> -        'keepalive' : True,
> -        'timeout' : None,
> -        'use_fast_upload': False,
> -        'poll_interval': 5,
> -        'krbservice': 'host',
> -        'cert': '~/.koji/client.crt',
> -        'ca': '~/.koji/clientca.crt',
> -        'serverca': '~/.koji/serverca.crt', 
> -        'authtype': None
> -        }
> -    #note: later config files override earlier ones
> -    configs = config_directory_contents('/etc/koji.conf.d')
> -    if os.access('/etc/koji.conf', os.F_OK):
> -        configs.append('/etc/koji.conf')
> -    if options.configFile:
> -        fn = os.path.expanduser(options.configFile)
> -        if os.path.isdir(fn):
> -            contents = config_directory_contents(fn)
> -            if not contents:
> -                parser.error("No config files found in directory: %s" % fn)
> -            configs.extend(contents)
> -        else:
> -            if not os.access(fn, os.F_OK):
> -                parser.error("No such file: %s" % fn)
> -            configs.append(fn)
> -    else:
> -        user_config_dir = os.path.expanduser("~/.koji/config.d")
> -        configs.extend(config_directory_contents(user_config_dir))
> -        fn = os.path.expanduser("~/.koji/config")
> -        if os.access(fn, os.F_OK):
> -            configs.append(fn)
> -    got_conf = False
> -    for configFile in configs:
> -        f = open(configFile)
> -        config = ConfigParser.ConfigParser()
> -        config.readfp(f)
> -        f.close()
> -        if config.has_section(options.profile):
> -            got_conf = True
> -            for name, value in config.items(options.profile):
> -                #note the defaults dictionary also serves to indicate which
> -                #options *can* be set via the config file. Such options should
> -                #not have a default value set in the option parser.
> -                if defaults.has_key(name):
> -                    if name in ('anon_retry', 'offline_retry', 'keepalive', 'use_fast_upload'):
> -                        defaults[name] = config.getboolean(options.profile, name)
> -                    elif name in ('max_retries', 'retry_interval',
> -                                  'offline_retry_interval', 'poll_interval', 'timeout'):
> -                        try:
> -                            defaults[name] = int(value)
> -                        except ValueError:
> -                            parser.error("value for %s config option must be a valid integer" % name)
> -                            assert False
> -                    else:
> -                        defaults[name] = value
> -    if configs and not got_conf:
> -        warn("Warning: no configuration for profile name: %s" % options.profile)
> -    for name, value in defaults.iteritems():
> -        if getattr(options, name, None) is None:
> -            setattr(options, name, value)
> -    dir_opts = ('topdir', 'cert', 'ca', 'serverca')
> -    for name in dir_opts:
> -        # expand paths here, so we don't have to worry about it later
> -        value = os.path.expanduser(getattr(options, name))
> -        setattr(options, name, value)
> -    
> +
> +    defaults = koji.read_config(options.profile, user_config=options.configFile)
> +    options._update_loose(defaults.__dict__)
> +
>      #honor topdir
>      if options.topdir:
>          koji.BASEDIR = options.topdir
> diff --git a/docs/profiles b/docs/profiles
> new file mode 100644
> index 0000000..82843fa
> --- /dev/null
> +++ b/docs/profiles
> @@ -0,0 +1,50 @@
> +Command Line Interface
> +----------------------
> +Koji client allows connecting to multiple koji instances from CLI
> +by using profiles. The default profile is given by executable file name,
> +which is 'koji'.
> +
> +To change koji profile, you can:
> +* run koji with --profile=$profile_name argument
> +* change executable file name by symlinking $profile_name -> koji
> +
> +
> +Python Modules
> +--------------
> +In order to refer to python koji modules by name,
> +you need to create new python module with following content:
> +
> +from koji import *
> +
> +BASEDIR = "/mnt/$profile_name-koji"
> +
> +
> +class PathInfo(PathInfo):
> +    @property
> +    def basedir(self):
> +        return BASEDIR
> +
> +
> +pathinfo = PathInfo()
> +config = read_config("$profile_name")
> +
> +
> +Then you can do following:
> +get koji profile_name from your config or code
> +>>> koji_module = __import__(profile_name)
> +>>> client = koji_module.ClientSession(koji_module.config.server, ...)
> +
> +
> +Configuration Files
> +-------------------
> +Configuration files are located in following locations:
> +* /etc/koji.conf
> +* /etc/koji.conf.d/*.conf
> +* ~/.koji/config.d/*.conf
> +* user-specified config
> +Koji reads them all, looking for [$profile_name] sections.
> +
> +
> +TODO
> +----
> +* consider using pyxdg for user config locations
> diff --git a/koji/__init__.py b/koji/__init__.py
> index a1fae97..2dd5e24 100644
> --- a/koji/__init__.py
> +++ b/koji/__init__.py
> @@ -1,7 +1,7 @@
>  # Python module
>  # Common functions
>  
> -# Copyright (c) 2005-2014 Red Hat, Inc.
> +# Copyright (c) 2005-2015 Red Hat, Inc.
>  #
>  #    Koji is free software; you can redistribute it and/or
>  #    modify it under the terms of the GNU Lesser General Public
> @@ -28,6 +28,8 @@ except ImportError:
>      sys.stderr.write("Warning: Could not install krbV module. Kerberos support will be disabled.\n")
>      sys.stderr.flush()
>  import base64
> +import ConfigParser
> +import optparse
>  import datetime
>  import errno
>  from fnmatch import fnmatch
> @@ -1436,6 +1438,110 @@ def openRemoteFile(relpath, topurl=None, topdir=None):
>      return fo
>  
>  
> +CONFIG_DEFAULTS = {
> +    'server': 'http://localhost/kojihub',
> +    'weburl': 'http://localhost/koji',
> +    'topurl': None,
> +    'pkgurl': None,
> +    'topdir': '/mnt/koji',
> +    'max_retries': None,
> +    'retry_interval': None,
> +    'anon_retry': None,
> +    'offline_retry': None,
> +    'offline_retry_interval': None,
> +    'keepalive': True,
> +    'timeout': None,
> +    'use_fast_upload': False,
> +    'poll_interval': 5,
> +    'krbservice': 'host',
> +    'cert': '~/.koji/client.crt',
> +    'ca': '~/.koji/clientca.crt',
> +    'serverca': '~/.koji/serverca.crt',
> +    'authtype': None
> +}
> +
> +INT_OPTIONS = ['max_retries', 'retry_interval', 'offline_retry_interval', 'poll_interval', 'timeout']
> +BOOL_OPTIONS = ['anon_retry', 'offline_retry', 'keepalive', 'use_fast_upload']
> +PATH_OPTIONS = ['topdir', 'cert', 'ca', 'serverca']
> +
> +def config_directory_contents(dir_name):
> +    configs = []
> +    try:
> +        conf_dir_contents = os.listdir(dir_name)
> +    except OSError, exception:
> +        if exception.errno != errno.ENOENT:
> +            raise
> +    else:
> +        for name in sorted(conf_dir_contents):
> +            if not name.endswith('.conf'):
> +                continue
> +            config_full_name = os.path.join(dir_name, name)
> +            configs.append(config_full_name)
> +    return configs
> +
> +
> +def read_config(instance_name, user_config=None):
> +    result = CONFIG_DEFAULTS.copy()
> +    for option in CONFIG_DEFAULTS:
> +        if option in PATH_OPTIONS:
> +            result[option] = os.path.expanduser(result[option])
> +
> +    configs = []
> +
> +    # main config
> +    configs.append("/etc/koji.conf")
> +
> +    # conf.d
> +    configs.extend(config_directory_contents("/etc/koji.conf.d"))
> +
> +    # user config
> +    configs.append(os.path.expanduser("~/.koji/config"))
> +
> +    # user conf.d
> +    configs.extend(config_directory_contents(os.path.expanduser("~/.koji/conf.d")))
> +
> +    # TODO: read configs via xdg.BaseDirectory.load_config_path("koji")
> +
> +    # user config specified in runtime
> +    if user_config is not None:
> +        configs.append(user_config)
> +
> +    # read configs in particular order, use the last value found
> +    for config_path in configs:
> +        if not os.access(config_path, os.F_OK):
> +            continue
> +        config = ConfigParser.SafeConfigParser()
> +        config.readfp(open(config_path, "r"))
> +
> +        if instance_name not in config.sections():
> +            continue
> +
> +        # check for invalid options
> +        invalid_options = []
> +        for option in config.options(instance_name):
> +            if option not in result:
> +                invalid_options.append(option)
> +
> +        if invalid_options:
> +            raise ValueError("Invalid options: %s" % ", ".join(invalid_options))
> +
> +        for option in config.options(instance_name):
> +            if option in BOOL_OPTIONS:
> +                result[option] = config.getboolean(instance_name, option)
> +            elif option in INT_OPTIONS:
> +                result[option] = config.getint(instance_name, option)
> +            else:
> +                result[option] = config.get(instance_name, option)
> +                if option in PATH_OPTIONS:
> +                    result[option] = os.path.expanduser(result[option])
> +
> +    # convert dict to optparse Values
> +    options = optparse.Values(result)
> +    return options
> +
> +config = read_config("koji")
> +
> +
>  class PathInfo(object):
>      # ASCII numbers and upper- and lower-case letter for use in tmpdir()
>      ASCII_CHARS = [chr(i) for i in range(48, 58) + range(65, 91) + range(97, 123)]
> @@ -1443,9 +1549,13 @@ class PathInfo(object):
>      def __init__(self, topdir=None):
>          self._topdir = topdir
>  
> +    @property
> +    def basedir(self):
> +        return str(BASEDIR)
> +
>      def topdir(self):
>          if self._topdir is None:
> -            self._topdir = str(BASEDIR)
> +            self._topdir = self.basedir
>          return self._topdir
>  
>      def _set_topdir(self, topdir):
> 



More information about the buildsys mailing list