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