Decentralized configuration.

The configuration is based on oslo-config, while this module lets
each module which want to be configured define the options it need
by itself, and get the limited access to those config variables.

Implements blueprint config-module

Change-Id: Iffbb859da0cb5fb8434bc439320bf6c20f9d39b5
This commit is contained in:
Zhihao Yuan 2013-03-05 18:16:42 -05:00
parent 6864efb0cb
commit 38d72dffd0
8 changed files with 262 additions and 15 deletions

View File

@ -13,7 +13,16 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from marconi.kernel import Kernel # NOQA # Import guard. No module level import during the setup procedure.
try:
__MARCONI_SETUP__
except NameError:
from marconi.kernel import Kernel # NOQA
else:
import sys as _sys
_sys.stderr.write('Running from marconi source directory.\n')
del _sys
import marconi.version import marconi.version
__version__ = marconi.version.version_info.deferred_version_string() __version__ = marconi.version.version_info.deferred_version_string()

194
marconi/common/config.py Normal file
View File

@ -0,0 +1,194 @@
# Copyright (c) 2013 Rackspace, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Decentralized configuration module.
A config variable `foo` is a read-only property accessible through
cfg.foo
, where `cfg` is either a global configuration accessible through
cfg = config.project('marconi').from_options(foo=("bar", "usage"), ...)
, or a local configuration associated with a namespace
cfg = config.namespace('drivers:transport:wsgi').from_options(port=80, ...)
The `from_options` call accepts a list of option definition, where each
option is represented as a keyword argument, in the form of either
`name=default` or `name=(default, description)`, where `name` is the
name of the option in a valid Python identifier, and `default` is the
default value of that option.
"""
from oslo.config import cfg
def _init():
"""Enclose an API specific config object."""
class ConfigProxy(object):
"""Prototype of the opaque config variable accessors."""
pass
class Obj(dict):
__getattr__ = dict.__getitem__
__setattr__ = dict.__setitem__
conf = cfg.ConfigOpts()
my = Obj(args=[])
def namespace(name, title=None):
"""
Create a config namespace.
:param name: the section name appears in the .ini file
:param title: an optional description
:returns: the option object for the namespace
"""
grp = cfg.OptGroup(name, title)
conf.register_group(grp)
def from_options(**opts):
"""
Define options under the associated namespace.
:returns: ConfigProxy of the associated namespace
"""
for k, v in opts.items():
conf.register_opt(_make_opt(k, v), group=grp)
def from_class(cls):
grant_access_to_class(conf[grp.name], cls)
return cls
return from_class(opaque_type_of(ConfigProxy, grp.name))()
return Obj(from_options=from_options)
def project(name=None):
"""
Access the global namespace.
:param name: the name of the project
:returns: a global option object
"""
def from_options(**opts):
"""
Define options under the global namespace.
:returns: ConfigProxy of the global namespace
"""
for k, v in opts.items():
conf.register_cli_opt(_make_opt(k, v))
@staticmethod
def set_cli(args):
"""
Save the CLI arguments.
:param args: a list of CLI arguments in strings
"""
my.args = []
my.args.extend(args)
@staticmethod
def load(config_file=None):
"""Load the configurations from a config file.
If the file name is not supplied, look for
/etc/%project/%project.conf
and
~/.%project/%project.conf
:param config_file: the name of an alternative config file
"""
if config_file is None:
conf(args=my.args, project=name, prog=name)
else:
conf(args=my.args, default_config_files=[config_file])
def from_class(cls):
grant_access_to_class(conf, cls)
cls.set_cli = set_cli
cls.load = load
return cls
return from_class(opaque_type_of(ConfigProxy, name))()
return Obj(from_options=from_options)
def opaque_type_of(base, postfix):
return type('%s of %s' % (base.__name__, postfix), (base,), {})
def grant_access_to_class(pairs, cls):
for k in pairs:
# A closure is needed for each %k to let
# different properties access different %k.
def let(k=k):
setattr(cls, k, property(lambda obj: pairs[k]))
let()
return namespace, project
namespace, project = _init()
def _make_opt(name, default):
"""
Create an oslo-config option with the type deduce from the %default
value of an option %name.
A default value of None is deduced to StrOpt; MultiStrOpt is not
supported.
:param name: the name of the option in a valid Python identifier
:param default: the default value of the option, or (default, description)
:raises: cfg.Error if the type can not be deduced.
"""
deduction = {
str: cfg.StrOpt,
bool: cfg.BoolOpt,
int: cfg.IntOpt,
long: cfg.IntOpt,
float: cfg.FloatOpt,
list: cfg.ListOpt,
}
if type(default) is tuple:
default, help = default
else:
help = None
if default is None:
return cfg.StrOpt(name, help=help)
try:
return deduction[type(default)](name, help=help, default=default)
except KeyError:
raise cfg.Error("unrecognized option type")

View File

@ -13,12 +13,14 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import ConfigParser from marconi.common import config
from marconi.storage import reference as storage from marconi.storage import reference as storage
from marconi.transport.wsgi import driver as wsgi from marconi.transport.wsgi import driver as wsgi
cfg = config.project('marconi').from_options()
class Kernel(object): class Kernel(object):
""" """
Defines the Marconi Kernel Defines the Marconi Kernel
@ -27,14 +29,13 @@ class Kernel(object):
lifetimes. lifetimes.
""" """
def __init__(self, config_file): def __init__(self, config_file=None):
#TODO(kgriffs): Error handling #TODO(kgriffs): Error handling
cfg = ConfigParser.SafeConfigParser() cfg.load(config_file)
cfg.read(config_file)
#TODO(kgriffs): Determine driver types from cfg #TODO(kgriffs): Determine driver types from cfg
self.storage = storage.Driver(cfg) self.storage = storage.Driver()
self.transport = wsgi.Driver(cfg, self.storage.queue_controller, self.transport = wsgi.Driver(self.storage.queue_controller,
self.storage.message_controller, self.storage.message_controller,
self.storage.claim_controller) self.storage.claim_controller)

View File

@ -18,9 +18,6 @@ from marconi import storage
class Driver(storage.DriverBase): class Driver(storage.DriverBase):
def __init__(self, cfg):
self._cfg = cfg
@property @property
def queue_controller(self): def queue_controller(self):
# TODO(kgriffs): Create base classes for controllers in common/ # TODO(kgriffs): Create base classes for controllers in common/

View File

@ -0,0 +1,41 @@
# Copyright (c) 2013 Rackspace, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import testtools
from marconi.common import config
from marconi.tests.util import suite
cfg = config.project().from_options(
without_help=3,
with_help=(None, "nonsense"))
class TestConfig(suite.TestSuite):
def test_cli(self):
args = ['--with_help', 'sense']
cfg.set_cli(args)
cfg.load(self.conf_path('wsgi_reference.conf'))
self.assertEquals(cfg.with_help, 'sense')
cfg.set_cli([])
cfg.load()
self.assertEquals(cfg.with_help, None)
def test_wrong_type(self):
ns = config.namespace('local')
with testtools.ExpectedException(config.cfg.Error):
ns.from_options(opt={})

View File

@ -13,16 +13,18 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from marconi.common import config
from marconi import transport from marconi import transport
cfg = config.namespace('drivers:transport:wsgi').from_options(port=8888)
class Driver(transport.DriverBase): class Driver(transport.DriverBase):
def __init__(self, cfg, queue_controller, message_controller, def __init__(self, queue_controller, message_controller,
claim_controller): claim_controller):
self._cfg = cfg
# E.g.: # E.g.:
# #
# self._queue_controller.create(tenant_id, queue_name) # self._queue_controller.create(tenant_id, queue_name)

View File

@ -14,8 +14,11 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import setuptools import setuptools
import __builtin__
__builtin__.__MARCONI_SETUP__ = None
from marconi.openstack.common import setup as common_setup from marconi.openstack.common import setup as common_setup
requires = common_setup.parse_requirements() requires = common_setup.parse_requirements()

View File

@ -1,2 +1,2 @@
cliff cliff
http://tarballs.openstack.org/oslo-config/oslo.config-1.1.0b1.tar.gz#egg=oslo.config oslo.config>=1.1.0