From: Nizamudeen A Date: Fri, 24 May 2024 14:20:11 +0000 (+0530) Subject: mgr/dashboard: apply replication policy for a bucket X-Git-Tag: testing/wip-mchangir-testing-uninline-20240614-143026-main-debug~3^2 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=8ae20324b61d140204b15a23ccedab178c6dcc7c;p=ceph-ci.git mgr/dashboard: apply replication policy for a bucket On a normal multisite configured cluster, you can create a bucket with this replication enabled which will stop the normal syncing and starts doing the granular bucket syncing; meaning only the bucket with the replication enabled will be syncing to the secondary site. To enable replication, there should be a group policy created in the primary site. If no group policy is there, the dashboard will create one with bidirectional rule and add all the zones in the zonegroup for syncing. Fixes: https://tracker.ceph.com/issues/66239 Signed-off-by: Nizamudeen A --- diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index f48dc592292..ab0f57509e0 100644 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -14,7 +14,7 @@ from ..rest_client import RequestException from ..security import Permission, Scope from ..services.auth import AuthManager, JwtManager from ..services.ceph_service import CephService -from ..services.rgw_client import NoRgwDaemonsException, RgwClient, RgwMultisite +from ..services.rgw_client import _SYNC_GROUP_ID, NoRgwDaemonsException, RgwClient, RgwMultisite from ..tools import json_str_to_object, str_to_bool from . import APIDoc, APIRouter, BaseController, CreatePermission, \ CRUDCollectionMethod, CRUDEndpoint, DeletePermission, Endpoint, \ @@ -242,6 +242,7 @@ class RgwDaemon(RESTController): 'server_hostname': hostname, 'realm_name': metadata['realm_name'], 'zonegroup_name': metadata['zonegroup_name'], + 'zonegroup_id': metadata['zonegroup_id'], 'zone_name': metadata['zone_name'], 'default': instance.daemon.name == metadata['id'], 'port': int(port) if port else None @@ -307,6 +308,8 @@ class RgwSite(RgwRESTController): return RgwClient.admin_instance(daemon_name=daemon_name).get_realms() if query == 'default-realm': return RgwClient.admin_instance(daemon_name=daemon_name).get_default_realm() + if query == 'default-zonegroup': + return RgwMultisite().get_all_zonegroups_info()['default_zonegroup'] # @TODO: for multisite: by default, retrieve cluster topology/map. raise DashboardException(http_status_code=501, component='rgw', msg='Not Implemented') @@ -396,6 +399,16 @@ class RgwBucket(RgwRESTController): rgw_client = RgwClient.instance(owner, daemon_name) return rgw_client.set_acl(bucket_name, acl) + def _set_replication(self, bucket_name: str, replication: bool, owner, daemon_name): + multisite = RgwMultisite() + rgw_client = RgwClient.instance(owner, daemon_name) + zonegroup_name = RgwClient.admin_instance(daemon_name=daemon_name).get_default_zonegroup() + + policy_exists = multisite.policy_group_exists(_SYNC_GROUP_ID, zonegroup_name) + if replication and not policy_exists: + multisite.create_dashboard_admin_sync_group(zonegroup_name=zonegroup_name) + return rgw_client.set_bucket_replication(bucket_name, replication) + @staticmethod def strip_tenant_from_bucket_name(bucket_name): # type (str) -> str @@ -463,9 +476,11 @@ class RgwBucket(RgwRESTController): lock_retention_period_days=None, lock_retention_period_years=None, encryption_state='false', encryption_type=None, key_id=None, tags=None, - bucket_policy=None, canned_acl=None, daemon_name=None): + bucket_policy=None, canned_acl=None, replication='false', + daemon_name=None): lock_enabled = str_to_bool(lock_enabled) encryption_state = str_to_bool(encryption_state) + replication = str_to_bool(replication) try: rgw_client = RgwClient.instance(uid, daemon_name) result = rgw_client.create_bucket(bucket, zonegroup, @@ -488,6 +503,8 @@ class RgwBucket(RgwRESTController): if canned_acl: self._set_acl(bucket, canned_acl, uid, daemon_name) + if replication: + self._set_replication(bucket, replication, uid, daemon_name) return result except RequestException as e: # pragma: no cover - handling is too obvious raise DashboardException(e, http_status_code=500, component='rgw') diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-daemon.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-daemon.ts index c685ba02700..179d7b5ab9a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-daemon.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-daemon.ts @@ -5,6 +5,7 @@ export class RgwDaemon { server_hostname: string; realm_name: string; zonegroup_name: string; + zonegroup_id: string; zone_name: string; default: boolean; port: number; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html index 551aad7ac55..22b094d6bd5 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html @@ -387,6 +387,49 @@ + +
+ Replication +
+ +
+ + + Enables replication for the objects in the bucket. + +
+ + Multi-site needs to be configured on the current realm or you need to be on + the default zonegroup to enable replication. + + + A bi-directional sync policy group will be created by the dashboard along with flows and pipes. + The pipe id will then be used for applying the replication policy to the bucket. + +
+
+
+
+
+ + +
+ Checking multi-site status... +
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts index 44318eda88e..34619824f20 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts @@ -307,4 +307,12 @@ describe('RgwBucketFormComponent', () => { expectValidLockInputs(false, 'Compliance', '2'); }); }); + + describe('bucket replication', () => { + it('should validate replication input', () => { + formHelper.setValue('replication', true); + fixture.detectChanges(); + formHelper.expectValid('replication'); + }); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts index 836ab3d301b..59f6dd6e399 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts @@ -10,7 +10,7 @@ import { AbstractControl, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import _ from 'lodash'; -import { forkJoin } from 'rxjs'; +import { Observable, forkJoin } from 'rxjs'; import * as xml2js from 'xml2js'; import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service'; @@ -36,6 +36,9 @@ import { RgwBucketVersioning } from '../models/rgw-bucket-versioning'; import { RgwConfigModalComponent } from '../rgw-config-modal/rgw-config-modal.component'; import { BucketTagModalComponent } from '../bucket-tag-modal/bucket-tag-modal.component'; import { TextAreaJsonFormatterService } from '~/app/shared/services/text-area-json-formatter.service'; +import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service'; +import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service'; +import { map, switchMap } from 'rxjs/operators'; @Component({ selector: 'cd-rgw-bucket-form', @@ -72,6 +75,8 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC ]; grantees: string[] = [Grantee.Owner, Grantee.Everyone, Grantee.AuthenticatedUsers]; aclPermissions: AclPermissionsType[] = [aclPermission.FullControl]; + multisiteStatus$: Observable; + isDefaultZoneGroup$: Observable; get isVersioningEnabled(): boolean { return this.bucketForm.getValue('versioning'); @@ -92,7 +97,9 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC private rgwEncryptionModal: RgwBucketEncryptionModel, private textAreaJsonFormatterService: TextAreaJsonFormatterService, public actionLabels: ActionLabelsI18n, - private readonly changeDetectorRef: ChangeDetectorRef + private readonly changeDetectorRef: ChangeDetectorRef, + private rgwMultisiteService: RgwMultisiteService, + private rgwDaemonService: RgwDaemonService ) { super(); this.editing = this.router.url.startsWith(`/rgw/bucket/${URLVerbs.EDIT}`); @@ -154,7 +161,8 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC lock_retention_period_days: [10, [CdValidators.number(false), lockDaysValidator]], bucket_policy: ['{}', CdValidators.json()], grantee: [Grantee.Owner, [Validators.required]], - aclPermission: [[aclPermission.FullControl], [Validators.required]] + aclPermission: [[aclPermission.FullControl], [Validators.required]], + replication: [false] }); } @@ -162,6 +170,16 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC const promises = { owners: this.rgwUserService.enumerate() }; + this.multisiteStatus$ = this.rgwMultisiteService.status(); + this.isDefaultZoneGroup$ = this.rgwDaemonService.selectedDaemon$.pipe( + switchMap((daemon) => + this.rgwSiteService.get('default-zonegroup').pipe( + map((defaultZoneGroup) => { + return daemon.zonegroup_id === defaultZoneGroup; + }) + ) + ) + ); this.kmsProviders = this.rgwEncryptionModal.kmsProviders; this.rgwBucketService.getEncryptionConfig().subscribe((data) => { @@ -331,7 +349,8 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC values['keyId'], xmlStrTags, bucketPolicy, - cannedAcl + cannedAcl, + values['replication'] ) .subscribe( () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts index bdb4decd9da..4936ee54a48 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts @@ -32,6 +32,7 @@ describe('RgwDaemonListComponent', () => { server_hostname: 'ceph', realm_name: 'realm1', zonegroup_name: 'zg1-realm1', + zonegroup_id: 'zg1-id', zone_name: 'zone1-zg1-realm1', default: true, port: 80 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts index 4f024f25f41..36cafa855a3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts @@ -26,6 +26,7 @@ describe('RgwOverviewDashboardComponent', () => { server_hostname: 'ceph', realm_name: 'realm1', zonegroup_name: 'zg1-realm1', + zonegroup_id: 'zg1-id', zone_name: 'zone1-zg1-realm1', default: true, port: 80 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts index 04755928b0a..248a59292d7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts @@ -51,7 +51,7 @@ import { BucketTagModalComponent } from './bucket-tag-modal/bucket-tag-modal.com CommonModule, SharedModule, FormsModule, - ReactiveFormsModule, + ReactiveFormsModule.withConfig({ callSetDisabledState: 'whenDisabledForLegacyCode' }), PerformanceCounterModule, NgbNavModule, RouterModule, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts index eaed2c4abac..f930af6cba3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts @@ -62,11 +62,12 @@ describe('RgwBucketService', () => { 'qwerty1', null, null, - 'private' + 'private', + 'true' ) .subscribe(); const req = httpTesting.expectOne( - `api/rgw/bucket?bucket=foo&uid=bar&zonegroup=default&lock_enabled=false&lock_mode=COMPLIANCE&lock_retention_period_days=5&encryption_state=true&encryption_type=aws%253Akms&key_id=qwerty1&tags=null&bucket_policy=null&canned_acl=private&${RgwHelper.DAEMON_QUERY_PARAM}` + `api/rgw/bucket?bucket=foo&uid=bar&zonegroup=default&lock_enabled=false&lock_mode=COMPLIANCE&lock_retention_period_days=5&encryption_state=true&encryption_type=aws%253Akms&key_id=qwerty1&tags=null&bucket_policy=null&canned_acl=private&replication=true&${RgwHelper.DAEMON_QUERY_PARAM}` ); expect(req.request.method).toBe('POST'); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts index ddeeadf5e49..8c9a9bacf4d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts @@ -62,7 +62,8 @@ export class RgwBucketService extends ApiClient { key_id: string, tags: string, bucketPolicy: string, - cannedAcl: string + cannedAcl: string, + replication: string ) { return this.rgwDaemonService.request((params: HttpParams) => { const paramsObject = { @@ -78,6 +79,7 @@ export class RgwBucketService extends ApiClient { tags: tags, bucket_policy: bucketPolicy, canned_acl: cannedAcl, + replication: replication, daemon_name: params.get('daemon_name') }; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts index da789d29fc4..9081c21e440 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts @@ -30,4 +30,8 @@ export class RgwMultisiteService { getSyncStatus() { return this.http.get(`${this.url}/sync_status`); } + + status() { + return this.http.get(`${this.uiUrl}/status`); + } } diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index dd907e1dc7f..d98613b53d6 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -10615,6 +10615,9 @@ paths: type: string placement_target: type: string + replication: + default: 'false' + type: string tags: type: string uid: diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py index fb6c83d60cb..0d7df8e31e8 100644 --- a/src/pybind/mgr/dashboard/services/rgw_client.py +++ b/src/pybind/mgr/dashboard/services/rgw_client.py @@ -29,6 +29,10 @@ except ImportError: logger = logging.getLogger('rgw_client') +_SYNC_GROUP_ID = 'dashboard_admin_group' +_SYNC_FLOW_ID = 'dashboard_admin_flow' +_SYNC_PIPE_ID = 'dashboard_admin_pipe' + class NoRgwDaemonsException(Exception): def __init__(self): @@ -605,6 +609,9 @@ class RgwClient(RestClient): return realm_info['name'] return None + def get_default_zonegroup(self): + return self.daemon.zonegroup_name + @RestClient.api_get('/{bucket_name}?versioning') def get_bucket_versioning(self, bucket_name, request=None): """ @@ -984,6 +991,38 @@ class RgwClient(RestClient): raise DashboardException(msg=msg, component='rgw') return retention_period_days, retention_period_years + @RestClient.api_put('/{bucket_name}?replication') + def set_bucket_replication(self, bucket_name, replication: bool, request=None): + # pGenerate the minimum replication configuration + # required for enabling the replication + root = ET.Element('ReplicationConfiguration', + xmlns='http://s3.amazonaws.com/doc/2006-03-01/') + role = ET.SubElement(root, 'Role') + role.text = f'{bucket_name}_replication_role' + + rule = ET.SubElement(root, 'Rule') + rule_id = ET.SubElement(rule, 'ID') + rule_id.text = _SYNC_PIPE_ID + + status = ET.SubElement(rule, 'Status') + status.text = 'Enabled' if replication else 'Disabled' + + filter_elem = ET.SubElement(rule, 'Filter') + prefix = ET.SubElement(filter_elem, 'Prefix') + prefix.text = '' + + destination = ET.SubElement(rule, 'Destination') + + bucket = ET.SubElement(destination, 'Bucket') + bucket.text = bucket_name + + replication_config = ET.tostring(root, encoding='utf-8', method='xml').decode() + + try: + request = request(data=replication_config) + except RequestException as e: + raise DashboardException(msg=str(e), component='rgw') + class SyncStatus(Enum): enabled = 'enabled' @@ -1655,8 +1694,8 @@ class RgwMultisite: rgw_realm_list = self.list_realms() rgw_zonegroup_list = self.list_zonegroups() rgw_zone_list = self.list_zones() - if len(rgw_realm_list['realms']) < 1 and len(rgw_zonegroup_list['zonegroups']) < 1 \ - and len(rgw_zone_list['zones']) < 1: + if len(rgw_realm_list['realms']) < 1 and len(rgw_zonegroup_list['zonegroups']) <= 1 \ + and len(rgw_zone_list['zones']) <= 1: is_multisite_configured = False return is_multisite_configured @@ -1772,10 +1811,13 @@ class RgwMultisite: except SubprocessError as error: raise DashboardException(error, http_status_code=500, component='rgw') - def get_sync_policy_group(self, group_id: str, bucket_name: str = ''): + def get_sync_policy_group(self, group_id: str, bucket_name: str = '', + zonegroup_name: str = ''): rgw_sync_policy_cmd = ['sync', 'group', 'get', '--group-id', group_id] if bucket_name: rgw_sync_policy_cmd += ['--bucket', bucket_name] + if zonegroup_name: + rgw_sync_policy_cmd += ['--rgw-zonegroup', zonegroup_name] try: exit_code, out, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd) if exit_code > 0: @@ -1922,3 +1964,30 @@ class RgwMultisite: http_status_code=500, component='rgw') except SubprocessError as error: raise DashboardException(error, http_status_code=500, component='rgw') + + def create_dashboard_admin_sync_group(self, zonegroup_name: str = ''): + + zonegroup_info = self.get_zonegroup(zonegroup_name) + zone_names = [] + for zones in zonegroup_info['zones']: + zone_names.append(zones['name']) + + # create a sync policy group with status allowed + self.create_sync_policy_group(_SYNC_GROUP_ID, SyncStatus.allowed.value) + # create a sync flow with source and destination zones + self.create_sync_flow(_SYNC_GROUP_ID, _SYNC_FLOW_ID, + SyncFlowTypes.symmetrical.value, + zones=zone_names) + # create a sync pipe with source and destination zones + self.create_sync_pipe(_SYNC_GROUP_ID, _SYNC_PIPE_ID, source_zones=['*'], + destination_zones=['*'], destination_buckets=['*']) + # period update --commit + self.update_period() + + def policy_group_exists(self, group_name: str, zonegroup_name: str): + try: + _ = self.get_sync_policy_group( + group_id=group_name, zonegroup_name=zonegroup_name) + return True + except DashboardException: + return False diff --git a/src/pybind/mgr/dashboard/tests/test_rgw.py b/src/pybind/mgr/dashboard/tests/test_rgw.py index b84838d1016..6e7a2960644 100644 --- a/src/pybind/mgr/dashboard/tests/test_rgw.py +++ b/src/pybind/mgr/dashboard/tests/test_rgw.py @@ -93,6 +93,7 @@ class RgwDaemonControllerTestCase(ControllerTestCase): 'id': 'daemon1', 'realm_name': 'realm1', 'zonegroup_name': 'zg1', + 'zonegroup_id': 'zg1-id', 'zone_name': 'zone1', 'frontend_config#0': 'beast port=80' }, @@ -101,6 +102,7 @@ class RgwDaemonControllerTestCase(ControllerTestCase): 'id': 'daemon2', 'realm_name': 'realm2', 'zonegroup_name': 'zg2', + 'zonegroup_id': 'zg2-id', 'zone_name': 'zone2', 'frontend_config#0': 'beast ssl_port=443 ssl_certificate=config:/config' }, @@ -109,6 +111,7 @@ class RgwDaemonControllerTestCase(ControllerTestCase): 'id': 'daemon3', 'realm_name': 'realm3', 'zonegroup_name': 'zg3', + 'zonegroup_id': 'zg3-id', 'zone_name': 'zone3', 'frontend_config#0': 'beast ssl_endpoint=0.0.0.0:8080 ssl_certificate=config:/config' @@ -118,6 +121,7 @@ class RgwDaemonControllerTestCase(ControllerTestCase): 'id': 'daemon4', 'realm_name': 'realm4', 'zonegroup_name': 'zg4', + 'zonegroup_id': 'zg4-id', 'zone_name': 'zone4', 'frontend_config#0': 'beast ssl_certificate=config:/config' }, @@ -126,6 +130,7 @@ class RgwDaemonControllerTestCase(ControllerTestCase): 'id': 'daemon5', 'realm_name': 'realm5', 'zonegroup_name': 'zg5', + 'zonegroup_id': 'zg5-id', 'zone_name': 'zone5', 'frontend_config#0': 'beast endpoint=0.0.0.0:8445 ssl_certificate=config:/config' @@ -139,6 +144,7 @@ class RgwDaemonControllerTestCase(ControllerTestCase): 'server_hostname': 'host1', 'realm_name': 'realm1', 'zonegroup_name': 'zg1', + 'zonegroup_id': 'zg1-id', 'zone_name': 'zone1', 'default': True, 'port': 80 }, @@ -149,6 +155,7 @@ class RgwDaemonControllerTestCase(ControllerTestCase): 'server_hostname': 'host1', 'realm_name': 'realm2', 'zonegroup_name': 'zg2', + 'zonegroup_id': 'zg2-id', 'zone_name': 'zone2', 'default': False, 'port': 443, @@ -160,6 +167,7 @@ class RgwDaemonControllerTestCase(ControllerTestCase): 'server_hostname': 'host1', 'realm_name': 'realm3', 'zonegroup_name': 'zg3', + 'zonegroup_id': 'zg3-id', 'zone_name': 'zone3', 'default': False, 'port': 8080, @@ -171,6 +179,7 @@ class RgwDaemonControllerTestCase(ControllerTestCase): 'server_hostname': 'host1', 'realm_name': 'realm4', 'zonegroup_name': 'zg4', + 'zonegroup_id': 'zg4-id', 'zone_name': 'zone4', 'default': False, 'port': None, @@ -182,6 +191,7 @@ class RgwDaemonControllerTestCase(ControllerTestCase): 'server_hostname': 'host1', 'realm_name': 'realm5', 'zonegroup_name': 'zg5', + 'zonegroup_id': 'zg5-id', 'zone_name': 'zone5', 'default': False, 'port': 8445,