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:
parent
d9cea01476
commit
2a645e9d0d
|
@ -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.
|
|
@ -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))
|
|
@ -0,0 +1 @@
|
|||
actions.py
|
|
@ -0,0 +1 @@
|
|||
actions.py
|
|
@ -0,0 +1 @@
|
|||
actions.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')
|
||||
|
|
|
@ -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')
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue