From e4d4d09b5393c45bbab92067f65274fe912de303 Mon Sep 17 00:00:00 2001 From: Chris MacNaughton Date: Thu, 19 Sep 2019 11:34:52 +0200 Subject: [PATCH] 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 --- README.md | 21 ++++++ config.yaml | 15 +++++ hooks/ceph_radosgw_context.py | 2 + hooks/hooks.py | 22 +++++-- hooks/upgrade-charm | 2 + hooks/upgrade-charm.real | 1 + templates/ceph.conf | 4 ++ tests/bundles/bionic-queens-namespaced.yaml | 44 +++++++++++++ tests/bundles/bionic-rocky-namespaced.yaml | 44 +++++++++++++ tests/bundles/bionic-stein-namespaced.yaml | 44 +++++++++++++ tests/bundles/xenial-mitaka-namespaced.yaml | 44 +++++++++++++ tests/tests.yaml | 4 ++ unit_tests/test_ceph_radosgw_context.py | 72 +++++++++++++++++++++ unit_tests/test_hooks.py | 66 +++++++++++++++++-- 14 files changed, 377 insertions(+), 8 deletions(-) create mode 120000 hooks/upgrade-charm.real create mode 100644 tests/bundles/bionic-queens-namespaced.yaml create mode 100644 tests/bundles/bionic-rocky-namespaced.yaml create mode 100644 tests/bundles/bionic-stein-namespaced.yaml create mode 100644 tests/bundles/xenial-mitaka-namespaced.yaml diff --git a/README.md b/README.md index 42b6d975..9d95be57 100644 --- a/README.md +++ b/README.md @@ -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. \ No newline at end of file diff --git a/config.yaml b/config.yaml index 322e07f5..a2791008 100644 --- a/config.yaml +++ b/config.yaml @@ -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. diff --git a/hooks/ceph_radosgw_context.py b/hooks/ceph_radosgw_context.py index f319c707..9cf29a1c 100644 --- a/hooks/ceph_radosgw_context.py +++ b/hooks/ceph_radosgw_context.py @@ -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 {} diff --git a/hooks/hooks.py b/hooks/hooks.py index 8debca8f..8a8a2760 100755 --- a/hooks/hooks.py +++ b/hooks/hooks.py @@ -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: diff --git a/hooks/upgrade-charm b/hooks/upgrade-charm index 71a85b0a..4ae2e75f 100755 --- a/hooks/upgrade-charm +++ b/hooks/upgrade-charm @@ -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 diff --git a/hooks/upgrade-charm.real b/hooks/upgrade-charm.real new file mode 120000 index 00000000..9416ca6a --- /dev/null +++ b/hooks/upgrade-charm.real @@ -0,0 +1 @@ +hooks.py \ No newline at end of file diff --git a/templates/ceph.conf b/templates/ceph.conf index c1ea349d..3d5bc34c 100644 --- a/templates/ceph.conf +++ b/templates/ceph.conf @@ -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 -%} diff --git a/tests/bundles/bionic-queens-namespaced.yaml b/tests/bundles/bionic-queens-namespaced.yaml new file mode 100644 index 00000000..9f005463 --- /dev/null +++ b/tests/bundles/bionic-queens-namespaced.yaml @@ -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 diff --git a/tests/bundles/bionic-rocky-namespaced.yaml b/tests/bundles/bionic-rocky-namespaced.yaml new file mode 100644 index 00000000..d57a78ad --- /dev/null +++ b/tests/bundles/bionic-rocky-namespaced.yaml @@ -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 diff --git a/tests/bundles/bionic-stein-namespaced.yaml b/tests/bundles/bionic-stein-namespaced.yaml new file mode 100644 index 00000000..e9bfd072 --- /dev/null +++ b/tests/bundles/bionic-stein-namespaced.yaml @@ -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 diff --git a/tests/bundles/xenial-mitaka-namespaced.yaml b/tests/bundles/xenial-mitaka-namespaced.yaml new file mode 100644 index 00000000..9fecfdd4 --- /dev/null +++ b/tests/bundles/xenial-mitaka-namespaced.yaml @@ -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 diff --git a/tests/tests.yaml b/tests/tests.yaml index db9d6630..cc0f6499 100644 --- a/tests/tests.yaml +++ b/tests/tests.yaml @@ -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 diff --git a/unit_tests/test_ceph_radosgw_context.py b/unit_tests/test_ceph_radosgw_context.py index adb56f79..d0f1c24c 100644 --- a/unit_tests/test_ceph_radosgw_context.py +++ b/unit_tests/test_ceph_radosgw_context.py @@ -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', diff --git a/unit_tests/test_hooks.py b/unit_tests/test_hooks.py index 45207e7c..66576aaf 100644 --- a/unit_tests/test_hooks.py +++ b/unit_tests/test_hooks.py @@ -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',