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
This commit is contained in:
Frode Nordahl 2019-03-05 10:37:55 +01:00
parent d9cea01476
commit 2a645e9d0d
No known key found for this signature in database
GPG Key ID: 6A5D59A3BA48373F
8 changed files with 271 additions and 0 deletions

18
src/actions.yaml Normal file
View File

@ -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.

103
src/actions/actions.py Executable file
View File

@ -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))

1
src/actions/demote Symbolic link
View File

@ -0,0 +1 @@
actions.py

1
src/actions/promote Symbolic link
View File

@ -0,0 +1 @@
actions.py

1
src/actions/refresh-pools Symbolic link
View File

@ -0,0 +1 @@
actions.py

View File

@ -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')

115
unit_tests/test_actions.py Normal file
View File

@ -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')

View File

@ -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()