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
This commit is contained in:
Liam Young 2021-08-24 16:56:03 +00:00
parent fb0565a3e1
commit fa1e41e2f8
6 changed files with 205 additions and 4 deletions

View File

@ -28,6 +28,7 @@ import multisite
from charmhelpers.core.hookenv import ( from charmhelpers.core.hookenv import (
relation_get, relation_get,
relation_id as ch_relation_id,
relation_ids, relation_ids,
related_units, related_units,
config, config,
@ -42,8 +43,10 @@ from charmhelpers.core.hookenv import (
is_leader, is_leader,
leader_set, leader_set,
leader_get, leader_get,
remote_service_name,
WORKLOAD_STATES, WORKLOAD_STATES,
) )
from charmhelpers.core.strutils import bool_from_string
from charmhelpers.fetch import ( from charmhelpers.fetch import (
apt_update, apt_update,
apt_install, apt_install,
@ -243,6 +246,9 @@ def config_changed():
for r_id in relation_ids('object-store'): for r_id in relation_ids('object-store'):
object_store_joined(r_id) object_store_joined(r_id)
for r_id in relation_ids('radosgw-user'):
radosgw_user_changed(r_id)
process_multisite_relations() process_multisite_relations()
CONFIGS.write_all() CONFIGS.write_all()
@ -367,6 +373,10 @@ def mon_relation(rid=None, unit=None):
zone)) zone))
service_restart(service_name()) service_restart(service_name())
for r_id in relation_ids('radosgw-user'):
radosgw_user_changed(r_id)
else: else:
send_request_if_needed(rq, relation='mon') send_request_if_needed(rq, relation='mon')
_mon_relation() _mon_relation()
@ -575,6 +585,91 @@ def certs_changed(relation_id=None, unit=None):
_certs_changed() _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') @hooks.hook('master-relation-joined')
def master_relation_joined(relation_id=None): def master_relation_joined(relation_id=None):
if not ready_for_service(legacy=False): if not ready_for_service(legacy=False):
@ -732,6 +827,8 @@ def leader_settings_changed():
if not is_leader(): if not is_leader():
for r_id in relation_ids('master'): for r_id in relation_ids('master'):
master_relation_joined(r_id) master_relation_joined(r_id)
for r_id in relation_ids('radosgw-user'):
radosgw_user_changed(r_id)
def process_multisite_relations(): def process_multisite_relations():

View File

@ -316,12 +316,48 @@ def tidy_defaults():
update_period() 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 :param username: username of user to create
:type username: str :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 :return: access key and secret
:rtype: (str, str) :rtype: (str, str)
""" """
@ -329,9 +365,10 @@ def create_system_user(username):
RGW_ADMIN, '--id={}'.format(_key_name()), RGW_ADMIN, '--id={}'.format(_key_name()),
'user', 'create', 'user', 'create',
'--uid={}'.format(username), '--uid={}'.format(username),
'--display-name=Synchronization User', '--display-name=Synchronization User'
'--system',
] ]
if system_user:
cmd.append('--system')
try: try:
result = json.loads(_check_output(cmd)) result = json.loads(_check_output(cmd))
return (result['keys'][0]['access_key'], return (result['keys'][0]['access_key'],
@ -340,6 +377,18 @@ def create_system_user(username):
return (None, None) 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): def pull_realm(url, access_key, secret):
""" """
Pull in a RADOS Gateway Realm from a master RGW instance Pull in a RADOS Gateway Realm from a master RGW instance

View File

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

View File

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

View File

@ -45,6 +45,8 @@ provides:
interface: radosgw-multisite interface: radosgw-multisite
object-store: object-store:
interface: swift-proxy interface: swift-proxy
radosgw-user:
interface: radosgw-user
peers: peers:
cluster: cluster:
interface: swift-ha interface: swift-ha

View File

@ -46,6 +46,7 @@ TO_PATCH = [
'relation_set', 'relation_set',
'relation_get', 'relation_get',
'related_units', 'related_units',
'remote_service_name',
'status_set', 'status_set',
'subprocess', 'subprocess',
'sys', 'sys',
@ -509,6 +510,56 @@ class CephRadosGWTests(CharmTestCase):
) )
mock_configure_https.assert_called_once_with() 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): class MiscMultisiteTests(CharmTestCase):