From 2a645e9d0d2499c6b40334eae454eadc0bb8ab5f Mon Sep 17 00:00:00 2001 From: Frode Nordahl Date: Tue, 5 Mar 2019 10:37:55 +0100 Subject: [PATCH] Add actions ``demote`` is used to demote all images in all pools, used for operator controlled fail over/fall back. ``promote`` is used to promote all images in all pools, used for operator controlled or disaster recovery fail over/fall back. ``refresh-pools`` is used to refresh list of eligible pools from local Ceph cluster. Side effect is to enable mirroring of pools created manually without the use of the charm ceph broker protocol. Change-Id: I9af983b37045f83a0a9703e2212b371b97dc3121 Depends-On: I97bfb9a2c0e30998566aee56d4630af6baa36d45 --- src/actions.yaml | 18 +++ src/actions/actions.py | 103 ++++++++++++++++++ src/actions/demote | 1 + src/actions/promote | 1 + src/actions/refresh-pools | 1 + src/reactive/ceph_rbd_mirror_handlers.py | 11 ++ unit_tests/test_actions.py | 115 ++++++++++++++++++++ unit_tests/test_ceph_rbd_mirror_handlers.py | 21 ++++ 8 files changed, 271 insertions(+) create mode 100644 src/actions.yaml create mode 100755 src/actions/actions.py create mode 120000 src/actions/demote create mode 120000 src/actions/promote create mode 120000 src/actions/refresh-pools create mode 100644 unit_tests/test_actions.py diff --git a/src/actions.yaml b/src/actions.yaml new file mode 100644 index 0000000..8325746 --- /dev/null +++ b/src/actions.yaml @@ -0,0 +1,18 @@ +demote: + description: | + Demote all primary images within all pools to non-primary. + params: + force: + type: boolean +promote: + description: | + Promote all non-primary images within all pools to primary. + params: + force: + type: boolean +refresh-pools: + description: | + \ + Refresh list of pools from local and remote Ceph endpoint. + As a side effect, mirroring will be configured for any manually created + pools that the charm currently does not know about. diff --git a/src/actions/actions.py b/src/actions/actions.py new file mode 100755 index 0000000..163d358 --- /dev/null +++ b/src/actions/actions.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +# Copyright 2019 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 os +import subprocess +import sys + +# Load basic layer module from $CHARM_DIR/lib +sys.path.append('lib') +from charms.layer import basic + +# setup module loading from charm venv +basic.bootstrap_charm_deps() + +import charms.reactive as reactive +import charmhelpers.core as ch_core +import charms_openstack.bus +import charms_openstack.charm + +# load reactive interfaces +reactive.bus.discover() +# load Endpoint based interface data +ch_core.hookenv._run_atstart() + +# load charm class +charms_openstack.bus.discover() + + +def rbd_mirror_action(args): + """Perform RBD command on pools in local Ceph endpoint.""" + action_name = os.path.basename(args[0]) + with charms_openstack.charm.provide_charm_instance() as charm: + ceph_local = reactive.endpoint_from_name('ceph-local') + pools = (pool for pool, attrs in ceph_local.pools.items() + if 'rbd' in attrs['applications']) + result = [] + cmd = ['rbd', '--id', charm.ceph_id, 'mirror', 'pool', action_name] + if ch_core.hookenv.action_get('force'): + cmd += ['--force'] + for pool in pools: + output = subprocess.check_output(cmd + [pool], + stderr=subprocess.STDOUT, + universal_newlines=True) + result.append('{}: {}'.format(pool, output.rstrip())) + ch_core.hookenv.action_set({'output': '\n'.join(result)}) + + +def refresh_pools(args): + """Refresh list of pools from Ceph. + + This is done by updating data on relations to ceph-mons which lead to them + updating the relation data they have with us as a response. + + Due to how the reactive framework handles publishing of relation data we + must do this by setting a flag and runnnig the reactive handlers, emulating + a full hook execution. + """ + if not reactive.is_flag_set('leadership.is_leader'): + ch_core.hookenv.action_fail('run action on the leader unit') + return + + # set and flush flag to disk + reactive.set_flag('refresh.pools') + ch_core.unitdata._KV.flush() + + # run reactive handlers to deal with flag + return reactive.main() + + +ACTIONS = { + 'demote': rbd_mirror_action, + 'promote': rbd_mirror_action, + 'refresh-pools': refresh_pools, +} + + +def main(args): + action_name = os.path.basename(args[0]) + try: + action = ACTIONS[action_name] + except KeyError: + return 'Action {} is undefined'.format(action_name) + + try: + action(args) + except Exception as e: + ch_core.hookenv.action_fail(str(e)) + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/src/actions/demote b/src/actions/demote new file mode 120000 index 0000000..405a394 --- /dev/null +++ b/src/actions/demote @@ -0,0 +1 @@ +actions.py \ No newline at end of file diff --git a/src/actions/promote b/src/actions/promote new file mode 120000 index 0000000..405a394 --- /dev/null +++ b/src/actions/promote @@ -0,0 +1 @@ +actions.py \ No newline at end of file diff --git a/src/actions/refresh-pools b/src/actions/refresh-pools new file mode 120000 index 0000000..405a394 --- /dev/null +++ b/src/actions/refresh-pools @@ -0,0 +1 @@ +actions.py \ No newline at end of file diff --git a/src/reactive/ceph_rbd_mirror_handlers.py b/src/reactive/ceph_rbd_mirror_handlers.py index 6ba018c..a31dd1c 100644 --- a/src/reactive/ceph_rbd_mirror_handlers.py +++ b/src/reactive/ceph_rbd_mirror_handlers.py @@ -86,6 +86,17 @@ def render_stuff(*args): charm_instance.assess_status() +@reactive.when('leadership.is_leader') +@reactive.when('refresh.pools') +@reactive.when('ceph-local.available') +@reactive.when('ceph-remote.available') +def refresh_pools(): + for endpoint in 'ceph-local', 'ceph-remote': + endpoint = reactive.endpoint_from_name(endpoint) + endpoint.refresh_pools() + reactive.clear_flag('refresh.pools') + + @reactive.when('leadership.is_leader') @reactive.when('config.rendered') @reactive.when('ceph-local.available') diff --git a/unit_tests/test_actions.py b/unit_tests/test_actions.py new file mode 100644 index 0000000..7fb3517 --- /dev/null +++ b/unit_tests/test_actions.py @@ -0,0 +1,115 @@ +# Copyright 2019 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 collections +import mock +import sys + +sys.modules['charms.layer'] = mock.MagicMock() +import actions.actions as actions +import charm.openstack.ceph_rbd_mirror as crm + +import charms_openstack.test_utils as test_utils + + +class TestCephRBDMirrorActions(test_utils.PatchHelper): + + def setUp(self): + super().setUp() + self.patch_release(crm.CephRBDMirrorCharm.release) + self.crm_charm = mock.MagicMock() + self.patch_object(actions.charms_openstack.charm, + 'provide_charm_instance', + new=mock.MagicMock()) + self.provide_charm_instance().__enter__.return_value = \ + self.crm_charm + self.provide_charm_instance().__exit__.return_value = None + + def test_rbd_mirror_action(self): + self.patch_object(actions.reactive, 'endpoint_from_name') + self.patch_object(actions.ch_core.hookenv, 'action_get') + self.patch_object(actions.subprocess, 'check_output') + self.patch_object(actions.ch_core.hookenv, 'action_set') + endpoint = mock.MagicMock() + endpoint.pools = collections.OrderedDict( + {'apool': {'applications': {'rbd': {}}}, + 'bpool': {'applications': {'rbd': {}}}}) + self.endpoint_from_name.return_value = endpoint + self.crm_charm.ceph_id = 'acephid' + self.action_get.return_value = False + self.check_output.return_value = 'Promoted 0 mirrored images\n' + actions.rbd_mirror_action(['promote']) + self.endpoint_from_name.assert_called_once_with('ceph-local') + self.action_get.assert_called_once_with('force') + self.check_output.assert_has_calls([ + mock.call(['rbd', '--id', 'acephid', 'mirror', 'pool', 'promote', + 'apool'], + stderr=actions.subprocess.STDOUT, + universal_newlines=True), + mock.call(['rbd', '--id', 'acephid', 'mirror', 'pool', 'promote', + 'bpool'], + stderr=actions.subprocess.STDOUT, + universal_newlines=True), + ], any_order=True) + # the order the pools has in the output string is undefined + self.action_set.assert_called_once_with( + {'output': mock.ANY}) + for entry in self.action_set.call_args[0][0]['output'].split('\n'): + assert (entry == 'apool: Promoted 0 mirrored images' or + entry == 'bpool: Promoted 0 mirrored images') + self.action_get.return_value = True + self.check_output.reset_mock() + actions.rbd_mirror_action(['promote']) + self.check_output.assert_has_calls([ + mock.call(['rbd', '--id', 'acephid', 'mirror', 'pool', 'promote', + '--force', 'apool'], + stderr=actions.subprocess.STDOUT, + universal_newlines=True), + mock.call(['rbd', '--id', 'acephid', 'mirror', 'pool', 'promote', + '--force', 'bpool'], + stderr=actions.subprocess.STDOUT, + universal_newlines=True), + ], any_order=True) + + def test_refresh_pools(self): + self.patch_object(actions.reactive, 'is_flag_set') + self.patch_object(actions.ch_core.hookenv, 'action_fail') + self.is_flag_set.return_value = False + actions.refresh_pools([]) + self.is_flag_set.assert_called_once_with('leadership.is_leader') + self.action_fail.assert_called_once_with( + 'run action on the leader unit') + self.is_flag_set.return_value = True + self.patch_object(actions.reactive, 'set_flag') + self.patch_object(actions.ch_core.unitdata, '_KV') + self.patch_object(actions.reactive, 'main') + actions.refresh_pools([]) + self.set_flag.assert_called_once_with('refresh.pools') + self._KV.flush.assert_called_once_with() + self.main.assert_called_once_with() + + def test_main(self): + self.patch_object(actions, 'ACTIONS') + self.patch_object(actions.ch_core.hookenv, 'action_fail') + args = ['/non-existent/path/to/charm/binary/promote'] + function = mock.MagicMock() + self.ACTIONS.__getitem__.return_value = function + actions.main(args) + function.assert_called_once_with(args) + self.ACTIONS.__getitem__.side_effect = KeyError + self.assertEqual(actions.main(args), 'Action promote is undefined') + self.ACTIONS.__getitem__.side_effect = None + function.side_effect = Exception('random exception') + actions.main(args) + self.action_fail.assert_called_once_with('random exception') diff --git a/unit_tests/test_ceph_rbd_mirror_handlers.py b/unit_tests/test_ceph_rbd_mirror_handlers.py index 0b5a5f7..3d798c1 100644 --- a/unit_tests/test_ceph_rbd_mirror_handlers.py +++ b/unit_tests/test_ceph_rbd_mirror_handlers.py @@ -45,6 +45,12 @@ class TestRegisteredHooks(test_utils.TestRegisteredHooks): 'ceph-local.available', 'ceph-remote.available', ), + 'refresh_pools': ( + 'leadership.is_leader', + 'refresh.pools', + 'ceph-local.available', + 'ceph-remote.available', + ), }, 'when_all': { 'request_keys': ( @@ -140,6 +146,21 @@ class TestCephRBDMirrorHandlers(test_utils.PatchHelper): ]) self.crm_charm.assess_status.assert_called_once_with() + def test_refresh_pools(self): + self.patch_object(handlers.reactive, 'endpoint_from_name') + self.patch_object(handlers.reactive, 'clear_flag') + endpoint_local = mock.MagicMock() + endpoint_remote = mock.MagicMock() + self.endpoint_from_name.side_effect = [endpoint_local, endpoint_remote] + handlers.refresh_pools() + self.endpoint_from_name.assert_has_calls([ + mock.call('ceph-local'), + mock.call('ceph-remote'), + ]) + endpoint_local.refresh_pools.assert_called_once_with() + endpoint_remote.refresh_pools.assert_called_once_with() + self.clear_flag.assert_called_once_with('refresh.pools') + def test_configure_pools(self): self.patch_object(handlers.reactive, 'endpoint_from_flag') endpoint_local = mock.MagicMock()