A Common Configuration Option Handling Module

The goal is to re-use as much common infrastructure between the various projects.

This blueprint specifically relates to the code for handling:

  1. command line option parsing
  2. common command line options
  3. configuration file parsing
  4. option value lookup

And concentrates on unifying Nova and Glance to begin with.

Command Line Option Parsing

Nova uses the gflags library for option parsing. The definition of command line options is spread across the project codebase, for example:

from nova import flags

FLAGS = flags.FLAGS
flags.DEFINE_bool('allow_admin_api',
    False,
    'When True, this API service will accept admin operations.')

if FLAGS.allow_admin_api:
    ...

The flag is only known about and parsed once the module which defines it is loaded. If a module needs to reference a flag defined in another module it does e.g.

FLAGS = flags.FLAGS
flags.DECLARE('num_iscsi_scan_tries', 'nova.volume.driver')

if tries >= FLAGS.num_iscsi_scan_tries:
    ...

Glance makes a much more limited used of command line options and favours using only its config file for most options. Presumably, the command line options are mostly for those options which may need to be set before the config file is loaded?

Glance uses the optparse library to define and parse these, so e.g.

oparser = optparse.OptionParser(version='%%prog %s'
                                % version.version_string())
...
group = optparse.OptionGroup(parser, "Common Options", help_text)
group.add_option('-v', '--verbose', default=False, dest="verbose",
                 action="store_true",
                 help="Print more verbose output")
...
parser.add_option_group(group)
...
(options, args) = parser.parse_args(cli_args)


return (vars(options), args)

This last step converts the attributes on the option values object into a dict, so each option is accessed by e.g.:

if options.get('verbose'):
    ...

Common Command Line Options

Glance's entire set of options is:

Nova has a much larger set of options. The options (roughly) in common with Glance are:

It seems pretty clear that a common logging module with a similar set of options to Glance's current options should suffice for Nova. Support for some options would be lost, but similar functionality would still be available via the use of the separate logging config file.

Configuration File Parsing

Nova uses the gflags format for its configuration file, consisting of a command line option per line. The vast majority of options are probably only ever set using this configuration file, rather than on the command line directly.

Glance's config files are PasteDeploy config files, one per WSGI app. However, in the case of glance-scrubber and glance-cache-{cleaner,prefetcher,pruner}, these aren't actually strictly WSGI apps but simply arbitrary objects loaded from a factory by PasteDeploy.

Before directly using Glance's direct approach, there are a few things worth considering:

  1. Using PasteDeploy for non-WSGI apps and their options doesn't seem right. Also, PasteDeploy's use of ConfigParser rather than SafeConfigParser appears to have caused some trouble. So, it may be a better approach to have the WSGI app configuration in a separate file (e.g. glance-paste.conf) from the rest of the configuration options which would be parsed using SafeConfigParser.

  2. While there are a relatively small number of configuration values shared between Glance services, there is quite a large number shared between Nova services. Although, it is not trivial to figure out which Nova services actually uses a given option. This suggests that it may be useful to support multiple configuration files with e.g. --config-file /etc/nova/nova-common.conf --config-file /etc/nova/nova-api.conf

  3. It is best to keep default values out of config files in /etc where possible. For example, with RPM, if a user installs Glance, sets verbose = True and, later, updates to a new version of Glance then the old configuration file will remain in place and the new one will be installed with a .rpmnew suffix. If we require that a sensible default for any given value exists in the configuration file, then things may break in this case. Best practice is to have the defaults in the code, but also included in the config file as comments.

Option Value Lookup

Glance's current approach involves passing the options dict around:

class ImageCache(object):
    def __init__(self, options):
        self.options = options
        ...
    def prune(self):
        max_size = int(self.options.get('image_cache_max_size',
                                        DEFAULT_MAX_CACHE_SIZE))
        ...

Option defaults, if required, are specified at option lookup time to dict.get().

Nova uses a global flag values object:

from nova import flags

FLAGS = flags.FLAGS
flags.DECLARE('num_iscsi_scan_tries', 'nova.volume.driver')

if tries >= FLAGS.num_iscsi_scan_tries:
    ...

and defaults are specified when the option is defined.

Globals aren't ideal and should be avoided, so we should go with Glance's approach of passing around the options. However, Nova would probably retain a global set of values until the codebase can be fully adapted.

However, Nova's approach of defining options and their defaults together in a structured way seems worthwhile.

Requirements

Requirements:

common_opts = [
    cfg.StrOpt('bind_host',
               default='0.0.0.0',
               help='IP address to listen on'),
    cfg.IntOpt('bind_port',
               default=9292,
               help='Port number to listen on')
]

enabled_apis_opt = \
    cfg.ListOpt('enabled_apis',
                default=['ec2', 'osapi'],
                help='List of APIs to enable by default')

DEFAULT_EXTENSIONS = [
    'nova.api.openstack.contrib.standard_extensions'
]
osapi_extension_opt = \
    cfg.MultiStrOpt('osapi_extension',
                    default=DEFAULT_EXTENSIONS)

class ExtensionManager(object):

    enabled_apis_opt = cfg.ListOpt(...)

    def __init__(self, conf):
        self.conf = conf
        self.conf.register_opt(enabled_apis_opt)
        ...

    def _load_extensions(self):
        for ext_factory in self.conf.osapi_extension:
            ....

opts = ...

def add_common_opts(conf):
    conf.register_opts(opts)

def get_bind_host(conf):
    return conf.bind_host

def get_bind_port(conf):
    return conf.bind_port

cli_opts = [
    cfg.BoolOpt('verbose',
                short='v',
                default=False,
                help='Print more verbose output'),
    cfg.BoolOpt('debug',
                short='d',
                default=False,
                help='Print debugging output'),
]

def add_common_opts(conf):
    conf.register_cli_opts(cli_opts)

class ConfigOpts(object):

    config_file_opt = \
        MultiStrOpt('config-file',
                    ...

    def __init__(self, ...):
        ...
        self.register_cli_opt(self.config_file_opt)

glance-api.conf:
  [DEFAULT]
  bind_port = 9292

glance.conf:
  [DEFAULT]
  bind_host = 0.0.0.0

conf = ConfigOpts()
conf(sys.argv[1:])
if conf.verbose:
    ...

rabbit_group = cfg.OptionGroup(name='rabbit', title='RabbitMQ options')

rabbit_host_opt = \
    cfg.StrOpt('host',
               default='localhost',
               help='IP/hostname to listen on'),
rabbit_port_opt = \
    cfg.IntOpt('port',
               default=5672,
               help='Port number to listen on')
rabbit_ssl_opt = \
    cfg.BoolOpt('use_ssl',
                default=False,
                help='Whether to support SSL connections')

def register_rabbit_opts(conf):
    conf.register_group(rabbit_group)
    # options can be registered under a group in any of these ways:
    conf.register_opt(rabbit_host_opt)
    conf.register_opt(rabbit_port_opt, group='rabbit')
    conf.register_opt(rabbit_ssl_opt, group=rabbit_group)

glance-api.conf:
  [DEFAULT]
  bind_port = 9292
  ...

  [rabbit]
  host = localhost
  port = 5672
  use_ssl = False
  userid = guest
  password = guest
  virtual_host = /

--rabbit-host localhost --rabbit-use-ssl False

server.start(app, conf.bind_port, conf.bind_host, conf)

self.connection = kombu.connection.BrokerConnection(
    hostname=conf.rabbit.host,
    port=conf.rabbit.port,
    ...)

opts = [
    cfg.StrOpt('state_path',
               default=os.path.join(os.path.dirname(__file__), '../'),
               help='Top-level directory for maintaining nova state'),
    cfg.StrOpt('sqlite_db',
               default='nova.sqlite',
               help='file name for sqlite'),
    cfg.StrOpt('sql_connection',
               default='sqlite:///$state_path/$sqlite_db',
               help='connection string for sql database'),
]

common_opts = [
    cfg.BoolOpt('verbose', ...),
    cfg.BoolOpt('debug', ...),
]

logging_opts = [
    cfg.StrOpt('log-config', ...),
    cfg.StrOpt('log-date-format', ...),
    ...
]

def CommonConfigOpts(object):

    def __init__(self):
        ...
        self.register_cli_opts(common_opts)
        self.register_cli_opts(logging_opts)

Wiki: CommonConfigModule (last edited 2011-12-05 23:15:15 by VishvanandaIshaya)