diff --git a/.stestr.conf b/.stestr.conf new file mode 100644 index 0000000..5fcccac --- /dev/null +++ b/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=./unit_tests +top_dir=./ diff --git a/src/reactive/magnum_handlers.py b/src/reactive/magnum_handlers.py index 566b0f8..46117e8 100644 --- a/src/reactive/magnum_handlers.py +++ b/src/reactive/magnum_handlers.py @@ -28,6 +28,7 @@ charm.use_defaults( @reactive.when('shared-db.available') @reactive.when('identity-service.available') @reactive.when('amqp.available') +@reactive.when_not('is-update-status-hook') def render_config(*interfaces): with charm.provide_charm_instance() as magnum_charm: magnum_charm.render_with_interfaces(interfaces) @@ -39,21 +40,26 @@ def render_config(*interfaces): @reactive.when('shared-db.available') @reactive.when('identity-service.available') @reactive.when('amqp.available') +@reactive.when_not('is-update-status-hook') def render_config_with_certs(amqp, keystone, shared_db, certs): with charm.provide_charm_instance() as magnum_charm: magnum_charm.configure_tls(certs) magnum_charm.render_with_interfaces( [amqp, keystone, shared_db, certs]) + magnum_charm.assess_status() + reactive.set_state('config.complete') @reactive.when('identity-service.connected') +@reactive.when_not('is-update-status-hook') def setup_endpoint(keystone): magnum.setup_endpoint(keystone) magnum.assess_status() -@reactive.when_not('leadership.set.magnum_password') @reactive.when('leadership.is_leader') +@reactive.when_not('leadership.set.magnum_password') +@reactive.when_not('is-update-status-hook') def generate_magnum_password(): passwd = binascii.b2a_hex(os.urandom(32)).decode() leadership.leader_set({'magnum_password': passwd}) @@ -62,6 +68,7 @@ def generate_magnum_password(): @reactive.when('leadership.set.magnum_password') @reactive.when('leadership.is_leader') @reactive.when('identity-service.available') +@reactive.when_not('is-update-status-hook') def write_openrc(): config = hookenv.config() ctx = context.IdentityServiceContext()() @@ -73,6 +80,7 @@ def write_openrc(): @reactive.when('config.complete') @reactive.when_not('db.synced') +@reactive.when_not('is-update-status-hook') def run_db_migration(): with charm.provide_charm_instance() as magnum_charm: magnum_charm.db_sync() @@ -83,6 +91,7 @@ def run_db_migration(): @reactive.when('ha.connected') @reactive.when_not('ha.available') +@reactive.when_not('is-update-status-hook') def connect_cluster(hacluster): with charm.provide_charm_instance() as magnum_charm: magnum_charm.configure_ha_resources(hacluster) diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py index e69de29..3836348 100644 --- a/unit_tests/__init__.py +++ b/unit_tests/__init__.py @@ -0,0 +1,72 @@ +# Copyright 2021 Canonical Ltd +# +# 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 sys + +sys.path.append('src') +sys.path.append('src/lib') + +# Mock out charmhelpers so that we can test without it. +import charms_openstack.test_mocks # noqa +charms_openstack.test_mocks.mock_charmhelpers() + +import mock + + +class _fake_decorator(object): + + def __init__(self, *args): + pass + + def __call__(self, f): + return f + + +charms = mock.MagicMock() +sys.modules['charms'] = charms +charms.leadership = mock.MagicMock() +sys.modules['charms.leadership'] = charms.leadership +charms.reactive = mock.MagicMock() +charms.reactive.when = _fake_decorator +charms.reactive.when_all = _fake_decorator +charms.reactive.when_any = _fake_decorator +charms.reactive.when_not = _fake_decorator +charms.reactive.when_none = _fake_decorator +charms.reactive.when_not_all = _fake_decorator +charms.reactive.not_unless = _fake_decorator +charms.reactive.when_file_changed = _fake_decorator +charms.reactive.collect_metrics = _fake_decorator +charms.reactive.meter_status_changed = _fake_decorator +charms.reactive.only_once = _fake_decorator +charms.reactive.hook = _fake_decorator +charms.reactive.bus = mock.MagicMock() +charms.reactive.flags = mock.MagicMock() +charms.reactive.relations = mock.MagicMock() +sys.modules['charms.reactive'] = charms.reactive +sys.modules['charms.reactive.bus'] = charms.reactive.bus +sys.modules['charms.reactive.bus'] = charms.reactive.decorators +sys.modules['charms.reactive.flags'] = charms.reactive.flags +sys.modules['charms.reactive.relations'] = charms.reactive.relations +keystoneauth1 = mock.MagicMock() +sys.modules['keystoneauth1'] = keystoneauth1 +netaddr = mock.MagicMock() +sys.modules['netaddr'] = netaddr +novaclient = mock.MagicMock() +sys.modules['novaclient'] = novaclient +neutron_lib = mock.MagicMock() +sys.modules['neutron_lib'] = neutron_lib +sys.modules['neutron_lib.constants'] = neutron_lib.constants +neutronclient = mock.MagicMock() +sys.modules['neutronclient'] = neutronclient +sys.modules['neutronclient.v2_0'] = neutronclient.v2_0 diff --git a/unit_tests/test_magnum_handlers.py b/unit_tests/test_magnum_handlers.py new file mode 100644 index 0000000..83bd87e --- /dev/null +++ b/unit_tests/test_magnum_handlers.py @@ -0,0 +1,129 @@ +# Copyright 2021 Canonical Ltd +# +# 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 mock + +import charm.openstack.magnum.magnum as magnum +import reactive.magnum_handlers as handlers + +import charms_openstack.test_utils as test_utils + + +class TestRegisteredHooks(test_utils.TestRegisteredHooks): + + def test_hooks(self): + defaults = [ + 'charm.installed', + 'amqp.connected', + 'shared-db.connected', + 'identity-service.connected', + 'config.changed', + 'update-status', + 'upgrade-charm', + 'certificates.available', + ] + hook_set = { + 'when': { + 'render_config': ( + 'shared-db.available', + 'identity-service.available', + 'amqp.available',), + 'render_config_with_certs': ( + 'certificates.available', + 'shared-db.available', + 'identity-service.available', + 'amqp.available',), + 'run_db_migration': ('config.complete',), + 'connect_cluster': ('ha.connected',), + 'generate_magnum_password': ('leadership.is_leader',), + 'generate_heartbeat_key': ('leadership.is_leader',), + 'setup_endpoint': ('identity-service.connected',), + 'write_openrc': ( + 'leadership.set.magnum_password', + 'leadership.is_leader', + 'identity-service.available',), + }, + 'when_not': { + 'run_db_migration': ('db.synced', 'is-update-status-hook'), + 'connect_cluster': ('ha.available', 'is-update-status-hook'), + 'generate_heartbeat_key': ('leadership.set.heartbeat-key', + 'is-update-status-hook'), + 'generate_magnum_password': ('leadership.set.magnum_password', + 'is-update-status-hook'), + 'setup_endpoint': ('is-update-status-hook',), + 'render_config': ('is-update-status-hook',), + 'render_config_with_certs': ('is-update-status-hook',), + 'write_openrc': ('is-update-status-hook',), + }, + } + # test that the hooks were registered via the + # reactive.magnum_handlers + self.registered_hooks_test_helper(handlers, hook_set, defaults) + + +class TestMagnumHandlers(test_utils.PatchHelper): + + def setUp(self): + super().setUp() + self.patch_release(magnum.MagnumCharm.release) + self.magnum_charm = mock.MagicMock() + self.patch_object(handlers.charm, 'provide_charm_instance', + new=mock.MagicMock()) + self.provide_charm_instance().__enter__.return_value = \ + self.magnum_charm + self.provide_charm_instance().__exit__.return_value = None + + def test_setup_endpoint(self): + keystone = mock.MagicMock() + charm = magnum.MagnumCharm.singleton + handlers.setup_endpoint(keystone) + keystone.register_endpoints.assert_called_once_with( + charm.service_type, + charm.region, + '{}/v1'.format(charm.public_url), + '{}/v1'.format(charm.internal_url), + '{}/v1'.format(charm.admin_url) + ) + + def test_render_config(self): + self.patch('charms.reactive.set_state', 'set_state') + handlers.render_config('arg1', 'arg2') + self.magnum_charm.render_with_interfaces.assert_called_once_with( + ('arg1', 'arg2')) + self.magnum_charm.assess_status.assert_called_once_with() + self.set_state.assert_called_once_with('config.complete') + + def test_render_config_with_certs(self): + self.patch('charms.reactive.set_state', 'set_state') + handlers.render_config_with_certs('arg1', 'arg2', 'arg3', 'arg4') + self.magnum_charm.configure_tls.assert_called_once_with('arg4') + self.magnum_charm.render_with_interfaces.assert_called_once_with( + ['arg1', 'arg2', 'arg3', 'arg4']) + self.magnum_charm.assess_status.assert_called_once_with() + self.set_state.assert_called_once_with('config.complete') + + def test_run_db_migration(self): + self.patch('charms.reactive.set_state', 'set_state') + handlers.run_db_migration() + self.magnum_charm.db_sync.assert_called_once_with() + self.magnum_charm.restart_all.assert_called_once_with() + self.set_state.assert_called_once_with('db.synced') + self.magnum_charm.assess_status.assert_called_once_with() + + def test_connect_cluster(self): + hacluster = mock.MagicMock() + handlers.connect_cluster(hacluster) + self.magnum_charm.configure_ha_resources.assert_called_once_with( + hacluster) + self.magnum_charm.assess_status.assert_called_once_with()