diff --git a/marconi/__init__.py b/marconi/__init__.py index d827ee913..a35be7283 100644 --- a/marconi/__init__.py +++ b/marconi/__init__.py @@ -13,7 +13,16 @@ # See the License for the specific language governing permissions and # 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 __version__ = marconi.version.version_info.deferred_version_string() diff --git a/marconi/common/config.py b/marconi/common/config.py new file mode 100644 index 000000000..9d92d659d --- /dev/null +++ b/marconi/common/config.py @@ -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") diff --git a/marconi/kernel.py b/marconi/kernel.py index 89711a151..cdfaf2d64 100644 --- a/marconi/kernel.py +++ b/marconi/kernel.py @@ -13,12 +13,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import ConfigParser - +from marconi.common import config from marconi.storage import reference as storage from marconi.transport.wsgi import driver as wsgi +cfg = config.project('marconi').from_options() + + class Kernel(object): """ Defines the Marconi Kernel @@ -27,14 +29,13 @@ class Kernel(object): lifetimes. """ - def __init__(self, config_file): + def __init__(self, config_file=None): #TODO(kgriffs): Error handling - cfg = ConfigParser.SafeConfigParser() - cfg.read(config_file) + cfg.load(config_file) #TODO(kgriffs): Determine driver types from cfg - self.storage = storage.Driver(cfg) - self.transport = wsgi.Driver(cfg, self.storage.queue_controller, + self.storage = storage.Driver() + self.transport = wsgi.Driver(self.storage.queue_controller, self.storage.message_controller, self.storage.claim_controller) diff --git a/marconi/storage/reference/driver.py b/marconi/storage/reference/driver.py index 10710e946..4d09c6f9a 100644 --- a/marconi/storage/reference/driver.py +++ b/marconi/storage/reference/driver.py @@ -18,9 +18,6 @@ from marconi import storage class Driver(storage.DriverBase): - def __init__(self, cfg): - self._cfg = cfg - @property def queue_controller(self): # TODO(kgriffs): Create base classes for controllers in common/ diff --git a/marconi/tests/test_config.py b/marconi/tests/test_config.py new file mode 100644 index 000000000..2b70e6540 --- /dev/null +++ b/marconi/tests/test_config.py @@ -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={}) diff --git a/marconi/transport/wsgi/driver.py b/marconi/transport/wsgi/driver.py index 9223dbcf7..d449fe533 100644 --- a/marconi/transport/wsgi/driver.py +++ b/marconi/transport/wsgi/driver.py @@ -13,16 +13,18 @@ # See the License for the specific language governing permissions and # limitations under the License. +from marconi.common import config from marconi import transport +cfg = config.namespace('drivers:transport:wsgi').from_options(port=8888) + + class Driver(transport.DriverBase): - def __init__(self, cfg, queue_controller, message_controller, + def __init__(self, queue_controller, message_controller, claim_controller): - self._cfg = cfg - # E.g.: # # self._queue_controller.create(tenant_id, queue_name) diff --git a/setup.py b/setup.py index 156c9f5b0..812129a1b 100755 --- a/setup.py +++ b/setup.py @@ -14,8 +14,11 @@ # 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 setuptools +import __builtin__ +__builtin__.__MARCONI_SETUP__ = None from marconi.openstack.common import setup as common_setup requires = common_setup.parse_requirements() diff --git a/tools/pip-requires b/tools/pip-requires index be6fbba4c..2c12b51fe 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -1,2 +1,2 @@ cliff -http://tarballs.openstack.org/oslo-config/oslo.config-1.1.0b1.tar.gz#egg=oslo.config +oslo.config>=1.1.0