Enable Ceph Radosgw tenant namespacing

This change enabled automatic tenant namespacing,
which also allows enabling global read permissions
on buckets.

Change-Id: Ic37c7161b7dddad49e3c2ab075d7e8b72f436b35
Closes-Bug: #1833072
This commit is contained in:
Chris MacNaughton 2019-09-19 11:34:52 +02:00 committed by Chris MacNaughton
parent 71b3e3b8eb
commit e4d4d09b53
14 changed files with 377 additions and 8 deletions

View File

@ -228,3 +228,24 @@ a zone that is currently read-only can be switched to read/write mode by either
promoting it to be the current master or by using the 'readwrite' action:
juju run-action -m us-east --wait rgw-us-east/0 readwrite
Tenant Namespacing
------------------
By default, Ceph Rados Gateway puts all tenant buckets into the same global
namespace, disallowing multiple tenants to have buckets with the same name.
Tenant namespacing can be enabled in this charm by deploying with configuration
like:
ceph-radosgw:
charm: cs:ceph-radosgw
num_units: 1
options:
namespace-tenants: True
Enabling tenant namespacing will place all tenant buckets into their own
namespace under their tenant id, as well as adding the tenant's ID parameter to
the Keystone endpoint registration to allow seamless integration with OpenStack.
Tenant namespacing cannot be toggled on in an existing installation as it will
remove tenant access to existing buckets. Toggling this option on an already
deployed Rados Gateway will have no effect.

View File

@ -317,3 +317,18 @@ options:
description: |
Name of RADOS Gateway Zone to create for multi-site replication. This
option must be specific to the local site e.g. us-west or us-east.
namespace-tenants:
type: boolean
default: False
description: |
Enable tenant namespacing. If tenant namespacing is enabled, keystone
tenants will be implicitly added to a matching tenant in radosgw, in
addition to updating the catalog URL to allow radosgw to support
publicly-readable containers and temporary URLS. This namespacing
also allows multiple tenants to create buckets with the same names,
as the bucket names are namespaced into the tenant namespaces in the
RADOS gateway.
This configuration option will not be enabled on a charm upgrade, and
cannot be toggled on in an existing installation as it will remove
tenant access to existing buckets.

View File

@ -33,6 +33,7 @@ from charmhelpers.core.hookenv import (
relation_get,
relation_ids,
unit_public_ip,
leader_get,
)
from charmhelpers.contrib.network.ip import (
format_ipv6_addr,
@ -104,6 +105,7 @@ class IdentityServiceContext(context.IdentityServiceContext):
if config('admin-roles'):
ctxt['user_roles'] += (',' + config('admin-roles'))
ctxt['cache_size'] = config('cache-size')
ctxt['namespace_tenants'] = leader_get('namespace_tenants')
if self.context_complete(ctxt):
return ctxt
return {}

View File

@ -170,6 +170,14 @@ def install():
install_packages()
if not os.path.exists('/etc/ceph'):
os.makedirs('/etc/ceph')
if is_leader():
leader_set(namespace_tenants=config('namespace-tenants'))
@hooks.hook('upgrade-charm.real')
def upgrade_charm():
if is_leader() and not leader_get('namespace_tenants'):
leader_set(namespace_tenants=False)
@hooks.hook('config-changed')
@ -294,10 +302,16 @@ def identity_joined(relid=None):
port = config('port')
admin_url = '%s:%i/swift' % (canonical_url(CONFIGS, ADMIN), port)
internal_url = '%s:%s/swift/v1' % \
(canonical_url(CONFIGS, INTERNAL), port)
public_url = '%s:%s/swift/v1' % \
(canonical_url(CONFIGS, PUBLIC), port)
if leader_get('namespace_tenants'):
internal_url = '%s:%s/swift/v1/AUTH_$(project_id)s' % \
(canonical_url(CONFIGS, INTERNAL), port)
public_url = '%s:%s/swift/v1/AUTH_$(project_id)s' % \
(canonical_url(CONFIGS, PUBLIC), port)
else:
internal_url = '%s:%s/swift/v1' % \
(canonical_url(CONFIGS, INTERNAL), port)
public_url = '%s:%s/swift/v1' % \
(canonical_url(CONFIGS, PUBLIC), port)
roles = [x for x in [config('operator-roles'), config('admin-roles')] if x]
requested_roles = ''
if roles:

View File

@ -7,3 +7,5 @@ find . -name '__pycache__' -prune -exec rm -rf "{}" \;
# Re-install dependencies to deal with py2->py3 switch for charm
./hooks/install_deps
./hooks/upgrade-charm.real

1
hooks/upgrade-charm.real Symbolic link
View File

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

View File

@ -55,6 +55,10 @@ rgw keystone accepted admin roles = {{ admin_roles }}
rgw keystone token cache size = {{ cache_size }}
rgw s3 auth use keystone = true
rgw s3 auth order = local, external
{% if namespace_tenants %}
rgw swift account in url = true
rgw keystone implicit tenants = true
{% endif %}
{% else -%}
rgw swift url = http://{{ unit_public_ip }}
{% endif -%}

View File

@ -0,0 +1,44 @@
options:
source: &source distro
series: bionic
applications:
ceph-radosgw:
charm: ceph-radosgw
num_units: 1
series: bionic
options:
source: *source
namespace-tenants: True
ceph-osd:
charm: cs:~openstack-charmers-next/ceph-osd
num_units: 3
constraints: "mem=2048"
storage:
osd-devices: 'cinder,10G'
options:
source: *source
osd-devices: '/srv/ceph /dev/test-non-existent'
ceph-mon:
charm: cs:~openstack-charmers-next/ceph-mon
num_units: 3
options:
source: *source
auth-supported: 'none'
percona-cluster:
charm: cs:~openstack-charmers-next/percona-cluster
num_units: 1
keystone:
expose: True
charm: cs:~openstack-charmers-next/keystone
num_units: 1
options:
openstack-origin: *source
relations:
- - keystone:shared-db
- percona-cluster:shared-db
- - ceph-osd:mon
- ceph-mon:osd
- - ceph-radosgw:mon
- ceph-mon:radosgw
- - ceph-radosgw:identity-service
- keystone:identity-service

View File

@ -0,0 +1,44 @@
options:
source: &source cloud:bionic-rocky
series: bionic
applications:
ceph-radosgw:
charm: ceph-radosgw
series: bionic
num_units: 1
options:
source: *source
namespace-tenants: True
ceph-osd:
charm: cs:~openstack-charmers-next/ceph-osd
num_units: 3
constraints: "mem=2048"
storage:
osd-devices: 'cinder,10G'
options:
source: *source
osd-devices: '/srv/ceph /dev/test-non-existent'
ceph-mon:
charm: cs:~openstack-charmers-next/ceph-mon
num_units: 3
options:
source: *source
auth-supported: 'none'
percona-cluster:
charm: cs:~openstack-charmers-next/percona-cluster
num_units: 1
keystone:
expose: True
charm: cs:~openstack-charmers-next/keystone
num_units: 1
options:
openstack-origin: *source
relations:
- - keystone:shared-db
- percona-cluster:shared-db
- - ceph-osd:mon
- ceph-mon:osd
- - ceph-radosgw:mon
- ceph-mon:radosgw
- - ceph-radosgw:identity-service
- keystone:identity-service

View File

@ -0,0 +1,44 @@
options:
source: &source cloud:bionic-stein
series: bionic
applications:
ceph-radosgw:
charm: ceph-radosgw
series: bionic
num_units: 1
options:
source: *source
namespace-tenants: True
ceph-osd:
charm: cs:~openstack-charmers-next/ceph-osd
num_units: 3
constraints: "mem=2048"
storage:
osd-devices: 'cinder,10G'
options:
source: *source
osd-devices: '/srv/ceph /dev/test-non-existent'
ceph-mon:
charm: cs:~openstack-charmers-next/ceph-mon
num_units: 3
options:
source: *source
auth-supported: 'none'
percona-cluster:
charm: cs:~openstack-charmers-next/percona-cluster
num_units: 1
keystone:
expose: True
charm: cs:~openstack-charmers-next/keystone
num_units: 1
options:
openstack-origin: *source
relations:
- - keystone:shared-db
- percona-cluster:shared-db
- - ceph-osd:mon
- ceph-mon:osd
- - ceph-radosgw:mon
- ceph-mon:radosgw
- - ceph-radosgw:identity-service
- keystone:identity-service

View File

@ -0,0 +1,44 @@
options:
source: &source distro
series: xenial
applications:
ceph-radosgw:
charm: ceph-radosgw
series: xenial
num_units: 1
options:
source: *source
namespace-tenants: True
ceph-osd:
charm: cs:~openstack-charmers-next/ceph-osd
num_units: 3
constraints: "mem=2048"
storage:
osd-devices: 'cinder,10G'
options:
source: *source
osd-devices: '/srv/ceph /dev/test-non-existent'
ceph-mon:
charm: cs:~openstack-charmers-next/ceph-mon
num_units: 3
options:
source: *source
auth-supported: 'none'
percona-cluster:
charm: cs:~openstack-charmers-next/percona-cluster
num_units: 1
keystone:
expose: True
charm: cs:~openstack-charmers-next/keystone
num_units: 1
options:
openstack-origin: *source
relations:
- - keystone:shared-db
- percona-cluster:shared-db
- - ceph-osd:mon
- ceph-mon:osd
- - ceph-radosgw:mon
- ceph-mon:radosgw
- - ceph-radosgw:identity-service
- keystone:identity-service

View File

@ -1,12 +1,16 @@
charm_name: ceph-radosgw
gate_bundles:
- bionic-stein
- bionic-stein-namespaced
- bionic-rocky
- bionic-rocky-namespaced
- bionic-queens
- bionic-queens-namespaced
- xenial-queens
- xenial-pike
- xenial-ocata
- xenial-mitaka
- xenial-mitaka-namespaced
- trusty-mitaka
smoke_bundles:
- bionic-stein

View File

@ -31,6 +31,7 @@ TO_PATCH = [
'unit_public_ip',
'determine_api_port',
'cmp_pkgrevno',
'leader_get',
]
@ -74,6 +75,7 @@ class IdentityServiceContextTest(CharmTestCase):
self.config.side_effect = self.test_config.get
self.maxDiff = None
self.cmp_pkgrevno.return_value = 1
self.leader_get.return_value = False
@patch.object(charmhelpers.contrib.openstack.context,
'filter_installed_packages', return_value=['absent-pkg'])
@ -124,6 +126,74 @@ class IdentityServiceContextTest(CharmTestCase):
'auth_port': 5432,
'auth_protocol': 'http',
'auth_type': 'keystone',
'namespace_tenants': False,
'cache_size': '42',
'service_host': '127.0.0.4',
'service_port': 9876,
'service_protocol': 'http',
}
if cmp_pkgrevno_side_effects and cmp_pkgrevno_side_effects[1] >= 0:
expect['user_roles'] = 'Babel'
expect['admin_roles'] = 'Dart'
else:
expect['user_roles'] = 'Babel,Dart'
if jewel_installed:
expect['auth_keystone_v3_supported'] = True
self.assertEqual(expect, ids_ctxt())
@patch.object(charmhelpers.contrib.openstack.context,
'filter_installed_packages', return_value=['absent-pkg'])
@patch.object(charmhelpers.contrib.openstack.context, 'format_ipv6_addr')
@patch.object(charmhelpers.contrib.openstack.context, 'context_complete')
@patch.object(charmhelpers.contrib.openstack.context, 'relation_get')
@patch.object(charmhelpers.contrib.openstack.context, 'related_units')
@patch.object(charmhelpers.contrib.openstack.context, 'relation_ids')
@patch.object(charmhelpers.contrib.openstack.context, 'log')
def test_ids_ctxt_with_namespace(self, _log, _rids, _runits, _rget,
_ctxt_comp, _format_ipv6_addr,
_filter_installed_packages,
jewel_installed=False,
cmp_pkgrevno_side_effects=None):
self.cmp_pkgrevno.side_effect = (cmp_pkgrevno_side_effects
if cmp_pkgrevno_side_effects
else [-1, -1])
self.test_config.set('operator-roles', 'Babel')
self.test_config.set('admin-roles', 'Dart')
self.test_config.set('cache-size', '42')
self.test_relation.set({'admin_token': 'ubuntutesting'})
self.relation_ids.return_value = ['identity-service:5']
self.related_units.return_value = ['keystone/0']
_format_ipv6_addr.return_value = False
_rids.return_value = 'rid1'
_runits.return_value = 'runit'
_ctxt_comp.return_value = True
self.leader_get.return_value = True
id_data = {
'service_port': 9876,
'service_host': '127.0.0.4',
'service_tenant_id': '2852107b8f8f473aaf0d769c7bbcf86b',
'service_domain_id': '8e50f28a556911e8aaeed33789425d23',
'auth_host': '127.0.0.5',
'auth_port': 5432,
'service_tenant': 'ten',
'service_username': 'admin',
'service_password': 'adminpass',
}
_rget.return_value = id_data
ids_ctxt = context.IdentityServiceContext()
expect = {
'admin_domain_id': '8e50f28a556911e8aaeed33789425d23',
'admin_password': 'adminpass',
'admin_tenant_id': '2852107b8f8f473aaf0d769c7bbcf86b',
'admin_tenant_name': 'ten',
'admin_token': 'ubuntutesting',
'admin_user': 'admin',
'api_version': '2.0',
'auth_host': '127.0.0.5',
'auth_port': 5432,
'auth_protocol': 'http',
'auth_type': 'keystone',
'namespace_tenants': True,
'cache_size': '42',
'service_host': '127.0.0.4',
'service_port': 9876,
@ -185,6 +255,7 @@ class IdentityServiceContextTest(CharmTestCase):
'auth_port': 5432,
'auth_protocol': 'http',
'auth_type': 'keystone',
'namespace_tenants': False,
'cache_size': '42',
'service_host': '127.0.0.4',
'service_port': 9876,
@ -247,6 +318,7 @@ class IdentityServiceContextTest(CharmTestCase):
'auth_port': 5432,
'auth_protocol': 'http',
'auth_type': 'keystone',
'namespace_tenants': False,
'cache_size': '42',
'service_domain_id': '8e50f28a556911e8aaeed33789425d23',
'service_host': '127.0.0.4',

View File

@ -145,11 +145,28 @@ class CephRadosGWTests(CharmTestCase):
ceph_hooks.APACHE_PACKAGES
)
def test_install(self):
@patch.object(ceph_hooks, 'leader_set')
@patch.object(ceph_hooks, 'is_leader')
def test_install(self, is_leader, leader_set):
_install_packages = self.patch('install_packages')
is_leader.return_value = True
ceph_hooks.install()
self.assertTrue(self.execd_preinstall.called)
self.assertTrue(_install_packages.called)
is_leader.assert_called_once()
leader_set.assert_called_once_with(namespace_tenants=False)
@patch.object(ceph_hooks, 'leader_set')
@patch.object(ceph_hooks, 'is_leader')
def test_install_without_namespacing(self, is_leader, leader_set):
_install_packages = self.patch('install_packages')
is_leader.return_value = True
self.test_config.set('namespace-tenants', True)
ceph_hooks.install()
self.assertTrue(self.execd_preinstall.called)
self.assertTrue(_install_packages.called)
is_leader.assert_called_once()
leader_set.assert_called_once_with(namespace_tenants=True)
@patch.object(ceph_hooks, 'certs_joined')
@patch.object(ceph_hooks, 'update_nrpe_config')
@ -231,19 +248,22 @@ class CephRadosGWTests(CharmTestCase):
ceph_hooks.gateway_relation()
self.relation_set.assert_called_with(hostname='10.0.0.1', port=80)
@patch.object(ceph_hooks, 'leader_get')
@patch('charmhelpers.contrib.openstack.ip.service_name',
lambda *args: 'ceph-radosgw')
@patch('charmhelpers.contrib.openstack.ip.config')
def test_identity_joined_early_version(self, _config):
def test_identity_joined_early_version(self, _config, _leader_get):
self.cmp_pkgrevno.return_value = -1
_leader_get.return_value = False
ceph_hooks.identity_joined()
self.sys.exit.assert_called_with(1)
@patch.object(ceph_hooks, 'leader_get')
@patch('charmhelpers.contrib.openstack.ip.service_name',
lambda *args: 'ceph-radosgw')
@patch('charmhelpers.contrib.openstack.ip.resolve_address')
@patch('charmhelpers.contrib.openstack.ip.config')
def test_identity_joined(self, _config, _resolve_address):
def test_identity_joined(self, _config, _resolve_address, _leader_get):
def _test_identify_joined(expected):
self.related_units = ['unit/0']
@ -251,6 +271,7 @@ class CephRadosGWTests(CharmTestCase):
_resolve_address.return_value = 'myserv'
_config.side_effect = self.test_config.get
self.test_config.set('region', 'region1')
_leader_get.return_value = False
ceph_hooks.identity_joined(relid='rid')
self.relation_set.assert_called_with(
service='swift',
@ -270,18 +291,55 @@ class CephRadosGWTests(CharmTestCase):
self.test_config.set('admin-roles', input.get('admin', ''))
_test_identify_joined(input['expected'])
@patch.object(ceph_hooks, 'leader_get')
@patch('charmhelpers.contrib.openstack.ip.service_name',
lambda *args: 'ceph-radosgw')
@patch('charmhelpers.contrib.openstack.ip.resolve_address')
@patch('charmhelpers.contrib.openstack.ip.config')
def test_identity_joined_namespaced(self, _config,
_resolve_address, _leader_get):
_leader_get.return_value = True
def _test_identify_joined(expected):
self.related_units = ['unit/0']
self.cmp_pkgrevno.return_value = 1
_resolve_address.return_value = 'myserv'
_config.side_effect = self.test_config.get
self.test_config.set('region', 'region1')
_leader_get.return_value = True
ceph_hooks.identity_joined(relid='rid')
self.relation_set.assert_called_with(
service='swift',
region='region1',
public_url='http://myserv:80/swift/v1/AUTH_$(project_id)s',
internal_url='http://myserv:80/swift/v1/AUTH_$(project_id)s',
requested_roles=expected,
relation_id='rid',
admin_url='http://myserv:80/swift')
inputs = [{'operator': 'foo', 'admin': 'bar', 'expected': 'foo,bar'},
{'operator': 'foo', 'expected': 'foo'},
{'admin': 'bar', 'expected': 'bar'},
{'expected': ''}]
for input in inputs:
self.test_config.set('operator-roles', input.get('operator', ''))
self.test_config.set('admin-roles', input.get('admin', ''))
_test_identify_joined(input['expected'])
@patch.object(ceph_hooks, 'leader_get')
@patch('charmhelpers.contrib.openstack.ip.service_name',
lambda *args: 'ceph-radosgw')
@patch('charmhelpers.contrib.openstack.ip.is_clustered')
@patch('charmhelpers.contrib.openstack.ip.unit_get')
@patch('charmhelpers.contrib.openstack.ip.config')
def test_identity_joined_public_name(self, _config, _unit_get,
_is_clustered):
_is_clustered, _leader_get):
self.related_units = ['unit/0']
_config.side_effect = self.test_config.get
self.test_config.set('os-public-hostname', 'files.example.com')
_unit_get.return_value = 'myserv'
_is_clustered.return_value = False
_leader_get.return_value = False
ceph_hooks.identity_joined(relid='rid')
self.relation_set.assert_called_with(
service='swift',