From fa1e41e2f8d181094d7bdad49dd40540d6643d7b Mon Sep 17 00:00:00 2001 From: Liam Young Date: Tue, 24 Aug 2021 16:56:03 +0000 Subject: [PATCH] Add radosgw-user relation Add a radosgw-user relation to allow charms to request a user. The requesting charm should supply the 'system-role' key in the app relation data bag to indicate whether the requested user should be a system user. This charm creates the user if it does not exist or looks up the users credentials if it does. The username and credentials are then passed back to the requestor via the app relation data bag. The units radosgw url and daemon id are also passed back this time using the unit relation data bag. Change-Id: Ieff1943b02f490559ccd245f60b744fb76a5d832 --- hooks/hooks.py | 97 ++++++++++++++++++++++++++++ hooks/multisite.py | 57 ++++++++++++++-- hooks/radosgw-user-relation-changed | 1 + hooks/radosgw-user-relation-departed | 1 + metadata.yaml | 2 + unit_tests/test_hooks.py | 51 +++++++++++++++ 6 files changed, 205 insertions(+), 4 deletions(-) create mode 120000 hooks/radosgw-user-relation-changed create mode 120000 hooks/radosgw-user-relation-departed diff --git a/hooks/hooks.py b/hooks/hooks.py index abc30460..36f99340 100755 --- a/hooks/hooks.py +++ b/hooks/hooks.py @@ -28,6 +28,7 @@ import multisite from charmhelpers.core.hookenv import ( relation_get, + relation_id as ch_relation_id, relation_ids, related_units, config, @@ -42,8 +43,10 @@ from charmhelpers.core.hookenv import ( is_leader, leader_set, leader_get, + remote_service_name, WORKLOAD_STATES, ) +from charmhelpers.core.strutils import bool_from_string from charmhelpers.fetch import ( apt_update, apt_install, @@ -243,6 +246,9 @@ def config_changed(): for r_id in relation_ids('object-store'): object_store_joined(r_id) + for r_id in relation_ids('radosgw-user'): + radosgw_user_changed(r_id) + process_multisite_relations() CONFIGS.write_all() @@ -367,6 +373,10 @@ def mon_relation(rid=None, unit=None): zone)) service_restart(service_name()) + + for r_id in relation_ids('radosgw-user'): + radosgw_user_changed(r_id) + else: send_request_if_needed(rq, relation='mon') _mon_relation() @@ -575,6 +585,91 @@ def certs_changed(relation_id=None, unit=None): _certs_changed() +def get_radosgw_username(r_id): + """Generate a username based on a relation id""" + gw_user = 'juju-' + r_id.replace(":", "-") + return gw_user + + +def get_radosgw_system_username(r_id): + """Generate a username for a system user based on a relation id""" + gw_user = get_radosgw_username(r_id) + # There is no way to switch a user from being a system user to a + # non-system user, so add the '-system' suffix to ensure there is + # no clash if the user request is updated in the future. + gw_user = gw_user + "-system" + return gw_user + + +@hooks.hook('radosgw-user-relation-departed') +def radosgw_user_departed(): + # If there are no related units then the last unit + # is currently departing. + if not related_units(): + r_id = ch_relation_id() + for user in [get_radosgw_system_username(r_id), + get_radosgw_username(r_id)]: + multisite.suspend_user(user) + + +@hooks.hook('radosgw-user-relation-changed') +def radosgw_user_changed(relation_id=None): + if not ready_for_service(legacy=False): + log('unit not ready, deferring radosgw_user configuration') + return + if relation_id: + r_ids = [relation_id] + else: + r_ids = relation_ids('radosgw-user') + # The leader manages the users and sets the credentials using the + # the application relation data bag. + if is_leader(): + for r_id in r_ids: + remote_app = remote_service_name(r_id) + relation_data = relation_get( + rid=r_id, + app=remote_app) + if 'system-role' not in relation_data: + log('system-role not in relation data, cannot create user', + level=DEBUG) + return + system_user = bool_from_string( + relation_data.get('system-role', 'false')) + if system_user: + gw_user = get_radosgw_system_username(r_id) + # If there is a pre-existing non-system user then ensure it is + # suspended + multisite.suspend_user(get_radosgw_username(r_id)) + else: + gw_user = get_radosgw_username(r_id) + # If there is a pre-existing system user then ensure it is + # suspended + multisite.suspend_user(get_radosgw_system_username(r_id)) + if gw_user in multisite.list_users(): + (access_key, secret_key) = multisite.get_user_creds(gw_user) + else: + (access_key, secret_key) = multisite.create_user( + gw_user, + system_user=system_user) + relation_set( + app=remote_app, + relation_id=r_id, + relation_settings={ + 'uid': gw_user, + 'access-key': access_key, + 'secret-key': secret_key}) + # Each unit publishes its own endpoint data and daemon id using the + # unit relation data bag. + for r_id in r_ids: + relation_set( + relation_id=r_id, + relation_settings={ + 'internal-url': "{}:{}".format( + canonical_url(CONFIGS, INTERNAL), + listen_port()), + 'daemon-id': socket.gethostname()}) + + @hooks.hook('master-relation-joined') def master_relation_joined(relation_id=None): if not ready_for_service(legacy=False): @@ -732,6 +827,8 @@ def leader_settings_changed(): if not is_leader(): for r_id in relation_ids('master'): master_relation_joined(r_id) + for r_id in relation_ids('radosgw-user'): + radosgw_user_changed(r_id) def process_multisite_relations(): diff --git a/hooks/multisite.py b/hooks/multisite.py index 18722423..df2638a3 100644 --- a/hooks/multisite.py +++ b/hooks/multisite.py @@ -316,12 +316,48 @@ def tidy_defaults(): update_period() -def create_system_user(username): +def get_user_creds(username): + cmd = [ + RGW_ADMIN, '--id={}'.format(_key_name()), + 'user', 'info', + '--uid={}'.format(username) + ] + result = json.loads(_check_output(cmd)) + return (result['keys'][0]['access_key'], + result['keys'][0]['secret_key']) + + +def suspend_user(username): """ - Create a RADOS Gateway system use for sync usage + Suspend a RADOS Gateway user :param username: username of user to create :type username: str + """ + if username not in list_users(): + hookenv.log( + "Cannot suspended user {}. User not found.".format(username), + level=hookenv.DEBUG) + return + cmd = [ + RGW_ADMIN, '--id={}'.format(_key_name()), + 'user', 'suspend', + '--uid={}'.format(username) + ] + _check_output(cmd) + hookenv.log( + "Suspended user {}".format(username), + level=hookenv.DEBUG) + + +def create_user(username, system_user=False): + """ + Create a RADOS Gateway user + + :param username: username of user to create + :type username: str + :param system_user: Whether to grant system user role + :type system_user: bool :return: access key and secret :rtype: (str, str) """ @@ -329,9 +365,10 @@ def create_system_user(username): RGW_ADMIN, '--id={}'.format(_key_name()), 'user', 'create', '--uid={}'.format(username), - '--display-name=Synchronization User', - '--system', + '--display-name=Synchronization User' ] + if system_user: + cmd.append('--system') try: result = json.loads(_check_output(cmd)) return (result['keys'][0]['access_key'], @@ -340,6 +377,18 @@ def create_system_user(username): return (None, None) +def create_system_user(username): + """ + Create a RADOS Gateway system user + + :param username: username of user to create + :type username: str + :return: access key and secret + :rtype: (str, str) + """ + create_user(username, system_user=True) + + def pull_realm(url, access_key, secret): """ Pull in a RADOS Gateway Realm from a master RGW instance diff --git a/hooks/radosgw-user-relation-changed b/hooks/radosgw-user-relation-changed new file mode 120000 index 00000000..9416ca6a --- /dev/null +++ b/hooks/radosgw-user-relation-changed @@ -0,0 +1 @@ +hooks.py \ No newline at end of file diff --git a/hooks/radosgw-user-relation-departed b/hooks/radosgw-user-relation-departed new file mode 120000 index 00000000..9416ca6a --- /dev/null +++ b/hooks/radosgw-user-relation-departed @@ -0,0 +1 @@ +hooks.py \ No newline at end of file diff --git a/metadata.yaml b/metadata.yaml index afbe5862..4d3b216b 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -45,6 +45,8 @@ provides: interface: radosgw-multisite object-store: interface: swift-proxy + radosgw-user: + interface: radosgw-user peers: cluster: interface: swift-ha diff --git a/unit_tests/test_hooks.py b/unit_tests/test_hooks.py index 796070dc..ebb17c5d 100644 --- a/unit_tests/test_hooks.py +++ b/unit_tests/test_hooks.py @@ -46,6 +46,7 @@ TO_PATCH = [ 'relation_set', 'relation_get', 'related_units', + 'remote_service_name', 'status_set', 'subprocess', 'sys', @@ -509,6 +510,56 @@ class CephRadosGWTests(CharmTestCase): ) mock_configure_https.assert_called_once_with() + @patch.object(ceph_hooks, 'canonical_url') + @patch.object(ceph_hooks, 'is_leader') + def test_radosgw_user_changed(self, is_leader, canonical_url): + relation_data = { + 'radosgw-user:3': {'system-role': 'false'}, + 'radosgw-user:5': {'system-role': 'true'}} + user = { + 'juju-radosgw-user-3': ('access1', 'key1'), + 'juju-radosgw-user-5-system': ('access2', 'key2')} + self.ready_for_service.return_value = True + is_leader.return_value = True + self.remote_service_name.return_value = 'ceph-dashboard' + canonical_url.return_value = 'http://radosgw' + self.listen_port.return_value = 80 + self.socket.gethostname.return_value = 'testinghostname' + self.relation_ids.return_value = relation_data.keys() + self.relation_get.side_effect = lambda rid, app: relation_data[rid] + self.multisite.list_users.return_value = ['juju-radosgw-user-3'] + self.multisite.get_user_creds.side_effect = lambda u: user[u] + self.multisite.create_user.side_effect = lambda u, system_user: user[u] + ceph_hooks.radosgw_user_changed() + expected = [ + call( + app='ceph-dashboard', + relation_id='radosgw-user:3', + relation_settings={ + 'uid': 'juju-radosgw-user-3', + 'access-key': 'access1', + 'secret-key': 'key1'}), + call( + app='ceph-dashboard', + relation_id='radosgw-user:5', + relation_settings={ + 'uid': 'juju-radosgw-user-5-system', + 'access-key': 'access2', + 'secret-key': 'key2'}), + call( + relation_id='radosgw-user:3', + relation_settings={ + 'internal-url': 'http://radosgw:80', + 'daemon-id': 'testinghostname'}), + call( + relation_id='radosgw-user:5', + relation_settings={ + 'internal-url': 'http://radosgw:80', + 'daemon-id': 'testinghostname'})] + self.relation_set.assert_has_calls( + expected, + any_order=True) + class MiscMultisiteTests(CharmTestCase):