diff --git a/oslo_config/cfg.py b/oslo_config/cfg.py index 30b6c3b2..5eee53c2 100644 --- a/oslo_config/cfg.py +++ b/oslo_config/cfg.py @@ -2042,6 +2042,7 @@ class ConfigOpts(collections.Mapping): self._oparser = None self._namespace = None self._mutable_ns = None + self._mutate_hooks = set([]) self.__cache = {} self._config_opts = [] self._cli_opts = collections.deque() @@ -2214,6 +2215,7 @@ class ConfigOpts(collections.Mapping): self._oparser = None self._namespace = None self._mutable_ns = None + # Keep _mutate_hooks self._validate_default_values = False self.unregister_opts(self._config_opts) for group in self._groups.values(): @@ -2819,12 +2821,25 @@ class ConfigOpts(collections.Mapping): self._namespace = namespace return True + def register_mutate_hook(self, hook): + """Registers a hook to be called by mutate_config_files. + + :param hook: a function accepting this ConfigOpts object and a dict of + config mutations, as returned by mutate_config_files. + :return None + """ + self._mutate_hooks.add(hook) + @__clear_cache def mutate_config_files(self): """Reload configure files and parse all options. Only options marked as 'mutable' will appear to change. + Hooks are called in a NON-DETERMINISTIC ORDER. Do not expect hooks to + be called in the same order as they were added. + + :return {(None or 'group', 'optname'): (old_value, new_value), ... } :raises Error if reloading fails """ @@ -2843,6 +2858,8 @@ class ConfigOpts(collections.Mapping): groupname = groupname if groupname else 'DEFAULT' LOG.info("Option %s.%s changed from [%s] to [%s]", groupname, optname, old, new) + for hook in self._mutate_hooks: + hook(self, fresh) return fresh def _warn_immutability(self): diff --git a/oslo_config/tests/test_cfg.py b/oslo_config/tests/test_cfg.py index da6de841..d0f2d9b1 100644 --- a/oslo_config/tests/test_cfg.py +++ b/oslo_config/tests/test_cfg.py @@ -1727,6 +1727,37 @@ class ConfigFileMutateTestCase(BaseTestCase): "Option group.boo changed from [old_boo] to [new_boo]\n") self.assertEqual(expected, self.log_fixture.output) + def test_hooks(self): + fresh = {} + result = [0] + + def foo(conf, foo_fresh): + self.assertEqual(self.conf, conf) + self.assertEqual(fresh, foo_fresh) + result[0] += 1 + + self.conf.register_mutate_hook(foo) + self.conf.register_mutate_hook(foo) + self._test_conf_files_mutate() + self.assertEqual(1, result[0]) + + def test_clear(self): + """Show that #clear doesn't undeclare opts. + + This justifies not clearing mutate_hooks either. ResetAndClearTestCase + shows that values are cleared. + """ + self.conf.register_cli_opt(cfg.StrOpt('cli')) + self.conf.register_opt(cfg.StrOpt('foo')) + dests = [info['opt'].dest for info, _ in self.conf._all_opt_infos()] + self.assertIn('cli', dests) + self.assertIn('foo', dests) + + self.conf.clear() + dests = [info['opt'].dest for info, _ in self.conf._all_opt_infos()] + self.assertIn('cli', dests) + self.assertIn('foo', dests) + class OptGroupsTestCase(BaseTestCase):