From a9cd5860035c7bec03732690439592c287c951d0 Mon Sep 17 00:00:00 2001 From: Liam Young Date: Thu, 20 Sep 2018 09:29:26 +0000 Subject: [PATCH] Add bus for auto-loading charm modules. charms_openstack.bus.discover() can now be used to load the openstack charm modules placed under lib/charm/openstack/. This is particularly useful when using the generic provide_charm_instance() code e.g. ``` charms_openstack.bus.discover() with charms_openstack.charm.provide_charm_instance() as ci: ci.some_charm_instance_method() ``` Change-Id: Ic1dfb58a37b3f283bbc5b31b5ea192527fc8e57d --- charms_openstack/bus.py | 80 +++++++++++++++++++++++++ unit_tests/test_charms_openstack_bus.py | 58 ++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 charms_openstack/bus.py create mode 100644 unit_tests/test_charms_openstack_bus.py diff --git a/charms_openstack/bus.py b/charms_openstack/bus.py new file mode 100644 index 0000000..c452522 --- /dev/null +++ b/charms_openstack/bus.py @@ -0,0 +1,80 @@ +# Copyright 2014-2018 Canonical Limited. +# +# This file is part of charms.reactive. +# +# charms.reactive is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 as +# published by the Free Software Foundation. +# +# charm-helpers is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with charm-helpers. If not, see . + +import importlib +import os +import charmhelpers.core.hookenv as hookenv + +# Code below is based on charms.reactive.bus + + +def discover(): + """Discover Openstack handlers based on convention. + + Handlers will be loaded from the following directory and its + subdirectories: + + * ``$CHARM_DIR/lib/charm/openstack`` + + The Python files will be imported and decorated functions registered. + """ + search_path = os.path.join( + hookenv.charm_dir(), 'lib', 'charm', 'openstack') + base_path = os.path.join(hookenv.charm_dir(), 'lib', 'charm') + for dirpath, dirnames, filenames in os.walk(search_path): + for filename in filenames: + filepath = os.path.join(dirpath, filename) + _register_handlers_from_file(base_path, filepath) + + +def _load_module(root, filepath): + """Import the supplied module. + + :param root: Module root directory eg directory that the import is + relative to. + :type root: str + :param filepath: Module file. + :type filepath: str + """ + assert filepath.startswith(root + os.sep) + assert filepath.endswith('.py') + package = os.path.basename(root) + module = filepath[len(root):-3].replace(os.sep, '.') + if module.endswith('.__init__'): + module = module[:-9] + + # Standard import. + importlib.import_module(package + module) + + +def _register_handlers_from_file(root, filepath): + """Import the supplied module if its a good candidate. + + :param root: Module root directory eg directory that the import is + relative to. + :type root: str + :param filepath: Module file. + :type filepath: str + """ + no_exec_blacklist = ( + '.md', '.yaml', '.txt', '.ini', + 'makefile', '.gitignore', + 'copyright', 'license') + if filepath.lower().endswith(no_exec_blacklist): + # Don't load handlers with one of the blacklisted extensions + return + if filepath.endswith('.py'): + _load_module(root, filepath) diff --git a/unit_tests/test_charms_openstack_bus.py b/unit_tests/test_charms_openstack_bus.py new file mode 100644 index 0000000..59ae460 --- /dev/null +++ b/unit_tests/test_charms_openstack_bus.py @@ -0,0 +1,58 @@ +# Copyright 2014-2018 Canonical Limited. +# +# This file is part of charms.reactive. +# +# charms.reactive is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 as +# published by the Free Software Foundation. +# +# charm-helpers is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with charm-helpers. If not, see . + +import os +import unittest + +import mock + +import charms_openstack.bus as bus + + +class TestBus(unittest.TestCase): + + @mock.patch.object(bus.os, 'walk') + @mock.patch.object(bus, '_register_handlers_from_file') + @mock.patch('charmhelpers.core.hookenv.charm_dir') + def test_discover(self, charm_dir, _register_handlers_from_file, walk): + os.walk.return_value = [( + '/x/unit-aodh-1/charm/lib/charm/openstack', + ['__pycache__'], + ['__init__.py', 'aodh.py'])] + + charm_dir.return_value = '/x/unit-aodh-1/charm' + bus.discover() + expect_calls = [ + mock.call( + '/x/unit-aodh-1/charm/lib/charm', + '/x/unit-aodh-1/charm/lib/charm/openstack/__init__.py'), + mock.call( + '/x/unit-aodh-1/charm/lib/charm', + '/x/unit-aodh-1/charm/lib/charm/openstack/aodh.py')] + _register_handlers_from_file.assert_has_calls(expect_calls) + + @mock.patch.object(bus.importlib, 'import_module') + def test_load_module(self, import_module): + import_module.side_effect = lambda x: x + bus._load_module( + '/x/charm/lib/charm', + '/x/charm/lib/charm/openstack/aodh.py'), + import_module.assert_called_once_with('charm.openstack.aodh') + + @mock.patch.object(bus, '_load_module') + def test_register_handlers_from_file(self, _load_module): + bus._register_handlers_from_file('reactive', 'reactive/foo.py') + _load_module.assert_called_once_with('reactive', 'reactive/foo.py')