Adds support for scaling down multisite rgw system

Adds implementation for relation-departed hooks to cleanly remove
participant sites from the multisite system. The replication
between zones is stopped and both zones split up to continue as
separate master zones.

Change-Id: I420f7933db55f3004f752949b5c09b1b79774f64
func-test-pr: https://github.com/openstack-charmers/zaza-openstack-tests/pull/863
This commit is contained in:
utkarshbhatthere 2022-08-30 10:36:53 +00:00
parent e97e3607e2
commit cb70cf4c5f
5 changed files with 188 additions and 9 deletions

View File

@ -821,6 +821,53 @@ def master_relation_joined(relation_id=None):
secret=secret) secret=secret)
@hooks.hook('master-relation-departed')
@hooks.hook('slave-relation-departed')
def multisite_relation_departed():
if not is_leader():
log('Cannot remove multisite relation, this unit is not the leader')
return
if not ready_for_service(legacy=False):
raise RuntimeError("Leader unit not ready for service.")
zone = config('zone')
zonegroup = config('zonegroup')
realm = config('realm')
# If config zone/zonegroup not present on site,
# remove-relation is called prematurely
if not multisite.is_multisite_configured(zone=zone,
zonegroup=zonegroup):
log('Multisite is not configured, skipping scaledown.')
return
zonegroup_info = multisite.get_zonegroup_info(zonegroup)
# remove other zones from zonegroup
for zone_info in zonegroup_info['zones']:
if zone_info['name'] is not zone:
multisite.remove_zone_from_zonegroup(
zone_info['name'], zonegroup
)
# modify self as master zone.
multisite.modify_zone(zone, default=True, master=True,
zonegroup=zonegroup)
# Update period.
multisite.update_period(
fatal=True, zonegroup=zonegroup,
zone=zone, realm=realm
)
# Verify multisite is not configured.
if multisite.is_multisite_configured(zone=zone,
zonegroup=zonegroup):
status_set(WORKLOAD_STATES.BLOCKED,
"Failed to do a clean scaledown.")
raise RuntimeError("Residual multisite config at local site.")
@hooks.hook('slave-relation-changed') @hooks.hook('slave-relation-changed')
def slave_relation_changed(relation_id=None, unit=None): def slave_relation_changed(relation_id=None, unit=None):
if not is_leader(): if not is_leader():

View File

@ -370,7 +370,60 @@ def modify_zone(name, endpoints=None, default=False, master=False,
return None return None
def update_period(fatal=True, zonegroup=None, zone=None): def remove_zone_from_zonegroup(zone, zonegroup):
"""Remove RADOS Gateway zone from provided parent zonegroup
Removal is different from deletion, this operation removes zone/zonegroup
affiliation but does not delete the actual zone.
:param zonegroup: parent zonegroup name
:type zonegroup: str
:param zone: zone name
:type zone: str
:return: modified zonegroup config
:rtype: dict
"""
cmd = [
RGW_ADMIN, '--id={}'.format(_key_name()),
'zonegroup', 'remove',
'--rgw-zonegroup={}'.format(zonegroup),
'--rgw-zone={}'.format(zone),
]
try:
result = _check_output(cmd)
return json.loads(result)
except (TypeError, subprocess.CalledProcessError) as exc:
raise RuntimeError(
"Error removing zone {} from zonegroup {}. Result: {}"
.format(zone, zonegroup, result)) from exc
def add_zone_to_zonegroup(zone, zonegroup):
"""Add RADOS Gateway zone to provided zonegroup
:param zonegroup: parent zonegroup name
:type zonegroup: str
:param zone: zone name
:type zone: str
:return: modified zonegroup config
:rtype: dict
"""
cmd = [
RGW_ADMIN, '--id={}'.format(_key_name()),
'zonegroup', 'add',
'--rgw-zonegroup={}'.format(zonegroup),
'--rgw-zone={}'.format(zone),
]
try:
result = _check_output(cmd)
return json.loads(result)
except (TypeError, subprocess.CalledProcessError) as exc:
raise RuntimeError(
"Error adding zone {} from zonegroup {}. Result: {}"
.format(zone, zonegroup, result)) from exc
def update_period(fatal=True, zonegroup=None, zone=None, realm=None):
"""Update RADOS Gateway configuration period """Update RADOS Gateway configuration period
:param fatal: In failure case, whether CalledProcessError is to be raised. :param fatal: In failure case, whether CalledProcessError is to be raised.
@ -379,6 +432,8 @@ def update_period(fatal=True, zonegroup=None, zone=None):
:type zonegroup: str :type zonegroup: str
:param zone: zone name :param zone: zone name
:type zone: str :type zone: str
:param realm: realm name
:type realm: str
""" """
cmd = [ cmd = [
RGW_ADMIN, '--id={}'.format(_key_name()), RGW_ADMIN, '--id={}'.format(_key_name()),
@ -388,6 +443,8 @@ def update_period(fatal=True, zonegroup=None, zone=None):
cmd.append('--rgw-zonegroup={}'.format(zonegroup)) cmd.append('--rgw-zonegroup={}'.format(zonegroup))
if zone is not None: if zone is not None:
cmd.append('--rgw-zone={}'.format(zone)) cmd.append('--rgw-zone={}'.format(zone))
if realm is not None:
cmd.append('--rgw-realm={}'.format(realm))
if fatal: if fatal:
_check_call(cmd) _check_call(cmd)
else: else:
@ -641,17 +698,21 @@ def is_multisite_configured(zone, zonegroup):
:rtype: Boolean :rtype: Boolean
""" """
if zone not in list_zones(): local_zones = list_zones()
hookenv.log("No local zone found with name ({})".format(zonegroup), if zone not in local_zones:
level=hookenv.ERROR) hookenv.log("zone {} not found in local zones {}"
.format(zone, local_zones), level=hookenv.ERROR)
return False return False
if zonegroup not in list_zonegroups(): local_zonegroups = list_zonegroups()
hookenv.log("No zonegroup found with name ({})".format(zonegroup), if zonegroup not in local_zonegroups:
level=hookenv.ERROR) hookenv.log("zonegroup {} not found in local zonegroups {}"
.format(zonegroup, local_zonegroups), level=hookenv.ERROR)
return False return False
sync_status = get_sync_status() sync_status = get_sync_status()
hookenv.log("Multisite sync status {}".format(sync_status),
level=hookenv.DEBUG)
if sync_status is not None: if sync_status is not None:
return ('data sync source:' in sync_status) return ('data sync source:' in sync_status)

View File

@ -219,7 +219,9 @@ def check_optional_config_and_relations(configs):
leader_get('restart_nonce')) leader_get('restart_nonce'))
# Any realm or zonegroup config is present, multisite checks can be done. # Any realm or zonegroup config is present, multisite checks can be done.
if (config('realm') or config('zonegroup')): # zone config can't be used because it's used by default.
if config('realm') or config('zonegroup') or relation_ids('master') \
or relation_ids('slave'):
# All of Realm, zonegroup, and zone must be configured. # All of Realm, zonegroup, and zone must be configured.
if not all(multisite_config): if not all(multisite_config):
return ('blocked', return ('blocked',

View File

@ -11,7 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import json
from unittest.mock import ( from unittest.mock import (
patch, call, MagicMock, ANY patch, call, MagicMock, ANY
) )
@ -75,6 +75,25 @@ TO_PATCH = [
] ]
# Stub Methods
def get_zonegroup_stub():
# populate dummy zones info
zone_one = {}
zone_one['id'] = "test_zone_id_one"
zone_one['name'] = "testzone"
zone_two = {}
zone_two['id'] = "test_zone_id_two"
zone_two['name'] = "testzone_two"
# populate dummy zonegroup info
zonegroup = {}
zonegroup['name'] = "testzonegroup"
zonegroup['master_zone'] = "test_zone_id_one"
zonegroup['zones'] = [zone_one, zone_two]
return zonegroup
class CephRadosGWTests(CharmTestCase): class CephRadosGWTests(CharmTestCase):
def setUp(self): def setUp(self):
@ -793,6 +812,26 @@ class MasterMultisiteTests(CephRadosMultisiteTests):
) )
self.multisite.list_realms.assert_not_called() self.multisite.list_realms.assert_not_called()
@patch.object(json, 'loads')
def test_multisite_relation_departed(self, json_loads):
for k, v in self._complete_config.items():
self.test_config.set(k, v)
self.is_leader.return_value = True
# Multisite is configured at first but then disabled.
self.multisite.is_multisite_configured.side_effect = [True, False]
self.multisite.get_zonegroup_info.return_value = get_zonegroup_stub()
# json.loads() raises TypeError for mock objects.
json_loads.returnvalue = []
ceph_hooks.multisite_relation_departed()
self.multisite.modify_zone.assert_called_once_with(
'testzone', default=True, master=True, zonegroup='testzonegroup'
)
self.multisite.update_period.assert_called_once_with(
fatal=True, zonegroup='testzonegroup',
zone='testzone', realm='testrealm'
)
class SlaveMultisiteTests(CephRadosMultisiteTests): class SlaveMultisiteTests(CephRadosMultisiteTests):

View File

@ -14,6 +14,7 @@
import inspect import inspect
import os import os
import json
from unittest import mock from unittest import mock
import multisite import multisite
@ -34,6 +35,7 @@ def get_zonegroup_stub():
# populate dummy zonegroup info # populate dummy zonegroup info
zonegroup = {} zonegroup = {}
zonegroup['name'] = "test_zonegroup" zonegroup['name'] = "test_zonegroup"
zonegroup['master_zone'] = "test_zone_id"
zonegroup['zones'] = [zone] zonegroup['zones'] = [zone]
return zonegroup return zonegroup
@ -441,6 +443,34 @@ class TestMultisiteHelpers(CharmTestCase):
'--rgw-zonegroup=test_zonegroup', '--rgw-zonegroup=test_zonegroup',
]) ])
@mock.patch.object(json, 'loads')
def test_remove_zone_from_zonegroup(self, json_loads):
# json.loads() raises TypeError for mock objects.
json_loads.returnvalue = []
multisite.remove_zone_from_zonegroup(
'test_zone', 'test_zonegroup',
)
self.subprocess.check_output.assert_called_with([
'radosgw-admin', '--id=rgw.testhost',
'zonegroup', 'remove', '--rgw-zonegroup=test_zonegroup',
'--rgw-zone=test_zone',
], stderr=mock.ANY)
@mock.patch.object(json, 'loads')
def test_add_zone_from_zonegroup(self, json_loads):
# json.loads() raises TypeError for mock objects.
json_loads.returnvalue = []
multisite.add_zone_to_zonegroup(
'test_zone', 'test_zonegroup',
)
self.subprocess.check_output.assert_called_with([
'radosgw-admin', '--id=rgw.testhost',
'zonegroup', 'add', '--rgw-zonegroup=test_zonegroup',
'--rgw-zone=test_zone',
], stderr=mock.ANY)
@mock.patch.object(multisite, 'list_zonegroups') @mock.patch.object(multisite, 'list_zonegroups')
@mock.patch.object(multisite, 'get_local_zone') @mock.patch.object(multisite, 'get_local_zone')
@mock.patch.object(multisite, 'list_buckets') @mock.patch.object(multisite, 'list_buckets')