Oslo/Config

= 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:


 * --verbose, --debug : set the log level to INFO or DEBUG, WARNING otherwise
 * --config-file : a .ini style configuration file
 * --log-config : python logging config
 * --log-date-format, --log-file, --log-dir, --use-syslog : other logging config, overwriting the logging config file if supplied
 * --use-syslog : log to syslog

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


 * --verbose : set log level to DEBUG, INFO otherwise
 * --flagfile : config file in gflags format
 * --use-syslog : log to syslog
 * --default_log_levels : log levels for individual modules
 * --logging_context_format_string, --logging_debug_format_suffix, --logging_default_format_string, --logging_exception_prefix: various formatting options

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.
 * 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   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.

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:


 * A way to define a schema for each config option - its name, type, optional group, default and description

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') ]


 * Config option types - string, integer, float, boolean, list, multistring

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)


 * Config option schemas are registered with with the config manager at runtime, but before the option is referenced

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: ....


 * Each config option schema should be defined in the module or class which uses the option

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


 * A config option can optionally be made available as a command line option; these must registered with the config manager before the command line is parsed (for the purposes of validation and --help)

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)


 * The config manager has a single CLI option defined by default, --config-file:

class ConfigOpts(object):

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

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


 * Option values are parsed from any supplied config files using SafeConfigParser. If none are specified, a default set is used e.g. ['glance-api.conf', 'glance.conf']

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

glance.conf: [DEFAULT] bind_host = 0.0.0.0


 * The parsing of CLI args and config files is initiated by invoking the config manager e.g.

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


 * Options can be registered as belonging to a group

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)


 * If no group is specified, options belong to the 'DEFAULT' section of config files

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

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


 * Command-line options in a group are automatically prefixed with the group name e.g.

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

- Option values in the default group are referenced as attributes/properties on the config manager object; groups are also attributes on the config manager, with attributes for each of the options associated with the group

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

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


 * String option values may use templates

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'), ]


 * The CommonConfigOpts config manager class allows a common set of config options to automatically be registered:

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)


 * PasteDeploy config is stored in a separate file