summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMark McLoughlin <markmc@redhat.com>2013-02-03 21:17:02 +0000
committerMark McLoughlin <markmc@redhat.com>2013-02-18 16:28:21 +0000
commitbcba9e708e2067caca316a8499aa659f9802eeec (patch)
treea7e69a8dd15fb5a3f60aed1688db4c22468abc32
parent538721d3fdab613ed3d3ab33df123f90da173e93 (diff)
downloadoslo-bcba9e708e2067caca316a8499aa659f9802eeec.tar.gz
oslo-bcba9e708e2067caca316a8499aa659f9802eeec.tar.xz
oslo-bcba9e708e2067caca316a8499aa659f9802eeec.zip
Add ConfigFilter wrapper class
Implements blueprint cfg-filter-view At the moment, if a module requires a configuration option from another module, we do: CONF.import_opt('source.module', 'option_name') but, in fact, all options from the imported module are available for use. The new ConfigFilter class makes it possible to enforce which options are available within a module e.g. with CONF = cfgfilter.ConfigFilter(cfg.CONF) CONF.import_opt('foo', 'source.module') CONF.register_opt(StrOpt('bar')) then the foo and bar options would be the only options available via this CONF object while still being available via the global cfg.CONF object. Change-Id: Ie3aa2cd090a626da8afd27ecb78853cbf279bc8b
-rw-r--r--openstack/common/cfgfilter.py241
-rw-r--r--tests/testmods/baar_baa_opt.py21
-rw-r--r--tests/testmods/fbar_foo_opt.py21
-rw-r--r--tests/testmods/fblaa_opt.py21
-rw-r--r--tests/unit/test_cfgfilter.py119
5 files changed, 423 insertions, 0 deletions
diff --git a/openstack/common/cfgfilter.py b/openstack/common/cfgfilter.py
new file mode 100644
index 0000000..f7b978b
--- /dev/null
+++ b/openstack/common/cfgfilter.py
@@ -0,0 +1,241 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, 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.
+
+r"""
+When using the global cfg.CONF object, it is quite common for a module
+to require the existence of configuration options registered by other
+modules.
+
+For example, if module 'foo' registers the 'blaa' option and the module
+'bar' uses the 'blaa' option then 'bar' might do:
+
+ import foo
+
+ print CONF.blaa
+
+However, it's completely non-obvious why foo is being imported (is it
+unused, can we remove the import) and where the 'blaa' option comes from.
+
+The CONF.import_opt() method allows such a dependency to be explicitly
+declared:
+
+ CONF.import_opt('blaa', 'foo')
+ print CONF.blaa
+
+However, import_opt() has a weakness - if 'bar' imports 'foo' using the
+import builtin and doesn't use import_opt() to import 'blaa', then 'blaa'
+can still be used without problems. Similarily, where multiple options
+are registered a module imported via importopt(), a lazy programmer can
+get away with only declaring a dependency on a single option.
+
+The ConfigFilter class provides a way to ensure that options are not
+available unless they have been registered in the module or imported using
+import_opt() e.g. with:
+
+ CONF = ConfigFilter(cfg.CONF)
+ CONF.import_opt('blaa', 'foo')
+ print CONF.blaa
+
+no other options other than 'blaa' are available via CONF.
+"""
+
+import collections
+import itertools
+
+from oslo.config import cfg
+
+
+class ConfigFilter(collections.Mapping):
+
+ """
+ A helper class which wraps a ConfigOpts object and enforces the
+ explicit declaration of dependencies on external options.
+ """
+
+ def __init__(self, conf):
+ """Construct a ConfigFilter object.
+
+ :param conf: a ConfigOpts object
+ """
+ self._conf = conf
+ self._opts = set()
+ self._groups = dict()
+
+ def __getattr__(self, name):
+ """Look up an option value.
+
+ :param name: the opt name (or 'dest', more precisely)
+ :returns: the option value (after string subsititution) or a GroupAttr
+ :raises: NoSuchOptError,ConfigFileValueError,TemplateSubstitutionError
+ """
+ if name in self._groups:
+ return self._groups[name]
+ if name not in self._opts:
+ raise cfg.NoSuchOptError(name)
+ return getattr(self._conf, name)
+
+ def __getitem__(self, key):
+ """Look up an option value."""
+ return getattr(self, key)
+
+ def __contains__(self, key):
+ """Return True if key is the name of a registered opt or group."""
+ return key in self._opts or key in self._groups
+
+ def __iter__(self):
+ """Iterate over all registered opt and group names."""
+ return itertools.chain(self._opts, self._groups.keys())
+
+ def __len__(self):
+ """Return the number of options and option groups."""
+ return len(self._opts) + len(self._groups)
+
+ def register_opt(self, opt, group=None):
+ """Register an option schema.
+
+ :param opt: an instance of an Opt sub-class
+ :param group: an optional OptGroup object or group name
+ :return: False if the opt was already registered, True otherwise
+ :raises: DuplicateOptError
+ """
+ if not self._conf.register_opt(opt, group):
+ return False
+
+ self._register_opt(opt.dest, group)
+ return True
+
+ def register_opts(self, opts, group=None):
+ """Register multiple option schemas at once."""
+ for opt in opts:
+ self.register_opt(opt, group)
+
+ def register_cli_opt(self, opt, group=None):
+ """Register a CLI option schema.
+
+ :param opt: an instance of an Opt sub-class
+ :param group: an optional OptGroup object or group name
+ :return: False if the opt was already register, True otherwise
+ :raises: DuplicateOptError, ArgsAlreadyParsedError
+ """
+ if not self._conf.register_cli_opt(opt, group):
+ return False
+
+ self._register_opt(opt.dest, group)
+ return True
+
+ def register_cli_opts(self, opts, group=None):
+ """Register multiple CLI option schemas at once."""
+ for opt in opts:
+ self.register_cli_opts(opt, group)
+
+ def register_group(self, group):
+ """Register an option group.
+
+ :param group: an OptGroup object
+ """
+ self._conf.register_group(group)
+ self._get_group(group.name)
+
+ def import_opt(self, opt_name, module_str, group=None):
+ """Import an option definition from a module.
+
+ :param name: the name/dest of the opt
+ :param module_str: the name of a module to import
+ :param group: an option OptGroup object or group name
+ :raises: NoSuchOptError, NoSuchGroupError
+ """
+ self._conf.import_opt(opt_name, module_str, group)
+ self._register_opt(opt_name, group)
+
+ def import_group(self, group, module_str):
+ """Import an option group from a module.
+
+ Note that this allows access to all options registered with
+ the group whether or not those options were registered by
+ the given module.
+
+ :param group: an option OptGroup object or group name
+ :param module_str: the name of a module to import
+ :raises: ImportError, NoSuchGroupError
+ """
+ self._conf.import_group(group, module_str)
+ group = self._get_group(group)
+ group._all_opts = True
+
+ def _register_opt(self, opt_name, group):
+ if group is None:
+ self._opts.add(opt_name)
+ return True
+ else:
+ group = self._get_group(group)
+ return group._register_opt(opt_name)
+
+ def _get_group(self, group_or_name):
+ if isinstance(group_or_name, cfg.OptGroup):
+ group_name = group_or_name.name
+ else:
+ group_name = group_or_name
+
+ if group_name in self._groups:
+ return self._groups[group_name]
+ else:
+ group = self.GroupAttr(self._conf, group_name)
+ self._groups[group_name] = group
+ return group
+
+ class GroupAttr(collections.Mapping):
+
+ """
+ A helper class representing the option values of a group as a mapping
+ and attributes.
+ """
+
+ def __init__(self, conf, group):
+ """Construct a GroupAttr object.
+
+ :param conf: a ConfigOpts object
+ :param group: an OptGroup object
+ """
+ self._conf = conf
+ self._group = group
+ self._opts = set()
+ self._all_opts = False
+
+ def __getattr__(self, name):
+ """Look up an option value."""
+ if not self._all_opts and name not in self._opts:
+ raise cfg.NoSuchOptError(name)
+ return getattr(self._conf[self._group], name)
+
+ def __getitem__(self, key):
+ """Look up an option value."""
+ return getattr(self, key)
+
+ def __contains__(self, key):
+ """Return True if key is the name of a registered opt or group."""
+ return key in self._opts
+
+ def __iter__(self):
+ """Iterate over all registered opt and group names."""
+ for key in self._opts:
+ yield key
+
+ def __len__(self):
+ """Return the number of options and option groups."""
+ return len(self._opts)
+
+ def _register_opt(self, opt_name):
+ self._opts.add(opt_name)
diff --git a/tests/testmods/baar_baa_opt.py b/tests/testmods/baar_baa_opt.py
new file mode 100644
index 0000000..bd0cef1
--- /dev/null
+++ b/tests/testmods/baar_baa_opt.py
@@ -0,0 +1,21 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 Red Hat, 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.
+
+from oslo.config import cfg
+
+CONF = cfg.CONF
+
+CONF.register_opt(cfg.StrOpt('baa'), group='baar')
diff --git a/tests/testmods/fbar_foo_opt.py b/tests/testmods/fbar_foo_opt.py
new file mode 100644
index 0000000..fb206c5
--- /dev/null
+++ b/tests/testmods/fbar_foo_opt.py
@@ -0,0 +1,21 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, 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.
+
+from oslo.config import cfg
+
+CONF = cfg.CONF
+
+CONF.register_opt(cfg.StrOpt('foo'), group='bbar')
diff --git a/tests/testmods/fblaa_opt.py b/tests/testmods/fblaa_opt.py
new file mode 100644
index 0000000..0c886af
--- /dev/null
+++ b/tests/testmods/fblaa_opt.py
@@ -0,0 +1,21 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, 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.
+
+from oslo.config import cfg
+
+CONF = cfg.CONF
+
+CONF.register_opt(cfg.StrOpt('fblaa'))
diff --git a/tests/unit/test_cfgfilter.py b/tests/unit/test_cfgfilter.py
new file mode 100644
index 0000000..e78acc8
--- /dev/null
+++ b/tests/unit/test_cfgfilter.py
@@ -0,0 +1,119 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2013 Red Hat, 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.
+
+from oslo.config.cfg import *
+
+from openstack.common.cfgfilter import *
+from tests import utils
+
+
+class ConfigFilterTestCase(utils.BaseTestCase):
+
+ def setUp(self):
+ super(ConfigFilterTestCase, self).setUp()
+ self.conf = ConfigOpts()
+ self.fconf = ConfigFilter(self.conf)
+
+ def test_register_opt_default(self):
+ self.fconf.register_opt(StrOpt('foo', default='bar'))
+
+ self.assertEquals(self.fconf.foo, 'bar')
+ self.assertEquals(self.fconf['foo'], 'bar')
+ self.assertTrue('foo' in self.fconf)
+ self.assertEquals(list(self.fconf), ['foo'])
+ self.assertEquals(len(self.fconf), 1)
+
+ def test_register_opt_none_default(self):
+ self.fconf.register_opt(StrOpt('foo'))
+
+ self.assertTrue(self.fconf.foo is None)
+ self.assertTrue(self.fconf['foo'] is None)
+ self.assertTrue('foo' in self.fconf)
+ self.assertEquals(list(self.fconf), ['foo'])
+ self.assertEquals(len(self.fconf), 1)
+
+ def test_register_grouped_opt_default(self):
+ self.fconf.register_opt(StrOpt('foo', default='bar'), group='blaa')
+
+ self.assertEquals(self.fconf.blaa.foo, 'bar')
+ self.assertEquals(self.fconf['blaa']['foo'], 'bar')
+ self.assertTrue('blaa' in self.fconf)
+ self.assertTrue('foo' in self.fconf.blaa)
+ self.assertEquals(list(self.fconf), ['blaa'])
+ self.assertEquals(list(self.fconf.blaa), ['foo'])
+ self.assertEquals(len(self.fconf), 1)
+ self.assertEquals(len(self.fconf.blaa), 1)
+
+ def test_register_grouped_opt_none_default(self):
+ self.fconf.register_opt(StrOpt('foo'), group='blaa')
+
+ self.assertTrue(self.fconf.blaa.foo is None)
+ self.assertTrue(self.fconf['blaa']['foo'] is None)
+ self.assertTrue('blaa' in self.fconf)
+ self.assertTrue('foo' in self.fconf.blaa)
+ self.assertEquals(list(self.fconf), ['blaa'])
+ self.assertEquals(list(self.fconf.blaa), ['foo'])
+ self.assertEquals(len(self.fconf), 1)
+ self.assertEquals(len(self.fconf.blaa), 1)
+
+ def test_register_group(self):
+ group = OptGroup('blaa')
+ self.fconf.register_group(group)
+ self.fconf.register_opt(StrOpt('foo'), group=group)
+
+ self.assertTrue(self.fconf.blaa.foo is None)
+ self.assertTrue(self.fconf['blaa']['foo'] is None)
+ self.assertTrue('blaa' in self.fconf)
+ self.assertTrue('foo' in self.fconf.blaa)
+ self.assertEquals(list(self.fconf), ['blaa'])
+ self.assertEquals(list(self.fconf.blaa), ['foo'])
+ self.assertEquals(len(self.fconf), 1)
+ self.assertEquals(len(self.fconf.blaa), 1)
+
+ def test_unknown_opt(self):
+ self.assertFalse('foo' in self.fconf)
+ self.assertEquals(len(self.fconf), 0)
+ self.assertRaises(NoSuchOptError, getattr, self.fconf, 'foo')
+
+ def test_blocked_opt(self):
+ self.conf.register_opt(StrOpt('foo'))
+
+ self.assertTrue('foo' in self.conf)
+ self.assertEquals(len(self.conf), 1)
+ self.assertTrue(self.conf.foo is None)
+ self.assertFalse('foo' in self.fconf)
+ self.assertEquals(len(self.fconf), 0)
+ self.assertRaises(NoSuchOptError, getattr, self.fconf, 'foo')
+
+ def test_import_opt(self):
+ self.fconf = ConfigFilter(CONF)
+ self.assertFalse(hasattr(self.fconf, 'blaa'))
+ self.fconf.import_opt('blaa', 'tests.testmods.blaa_opt')
+ self.assertTrue(hasattr(self.fconf, 'blaa'))
+
+ def test_import_opt_in_group(self):
+ self.fconf = ConfigFilter(CONF)
+ self.assertFalse(hasattr(self.fconf, 'bar'))
+ self.fconf.import_opt('foo', 'tests.testmods.bar_foo_opt', group='bar')
+ self.assertTrue(hasattr(self.fconf, 'bar'))
+ self.assertTrue(hasattr(self.fconf.bar, 'foo'))
+
+ def test_import_group(self):
+ self.fconf = ConfigFilter(CONF)
+ self.assertFalse(hasattr(self.fconf, 'baar'))
+ self.fconf.import_group('baar', 'tests.testmods.baar_baa_opt')
+ self.assertTrue(hasattr(self.fconf, 'baar'))
+ self.assertTrue(hasattr(self.fconf.baar, 'baa'))