]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: apply replication policy for a bucket 57713/head
authorNizamudeen A <nia@redhat.com>
Fri, 24 May 2024 14:20:11 +0000 (19:50 +0530)
committerNizamudeen A <nia@redhat.com>
Tue, 11 Jun 2024 16:43:57 +0000 (22:13 +0530)
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 <nia@redhat.com>
14 files changed:
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-daemon.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-form/rgw-bucket-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-bucket.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rgw_client.py
src/pybind/mgr/dashboard/tests/test_rgw.py

index f48dc592292f7c5a6157fe8da3b571e10182e1a2..ab0f57509e071159622c27def9e98839284c6415 100644 (file)
@@ -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')
index c685ba0270089bbbf73844d20223d55c0f956c32..179d7b5ab9ac7f5d506544ef28b1a85280998add 100644 (file)
@@ -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;
index 551aad7ac5582c2b9c4ad7f8bdef45ca5ccfe32d..22b094d6bd54df469f2068b23e08b49b8160373a 100644 (file)
           </div>
         </fieldset>
 
+        <!-- Replication -->
+        <fieldset>
+          <legend class="cd-header"
+                  i18n>Replication</legend>
+          <div class="form-group row">
+            <label class="cd-col-form-label pt-0"
+                   for="replication"
+                   i18n>
+                    Enable
+            </label>
+            <div class="cd-col-form-input"
+                 *ngIf="{status: multisiteStatus$, isDefaultZg: isDefaultZoneGroup$ | async} as multisiteStatus; else loadingTpl">
+              <input type="checkbox"
+                     class="form-check-input"
+                     id="replication"
+                     name="replication"
+                     formControlName="replication"
+                     [attr.disabled]="!multisiteStatus.isDefaultZg && !multisiteStatus.status.available ? true : null">
+              <cd-help-text>
+                <span i18n>Enables replication for the objects in the bucket.</span>
+              </cd-help-text>
+              <div class="mt-1">
+                <cd-alert-panel type="info"
+                                *ngIf="!multisiteStatus.status.available && !multisiteStatus.isDefaultZg"
+                                class="me-1"
+                                id="multisite-configured-info"
+                                i18n>
+                  Multi-site needs to be configured on the current realm or you need to be on
+                  the default zonegroup to enable replication.
+                </cd-alert-panel>
+                <cd-alert-panel type="info"
+                                *ngIf="bucketForm.getValue('replication')"
+                                class="me-1"
+                                id="replication-info"
+                                i18n>
+                  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.
+                </cd-alert-panel>
+              </div>
+            </div>
+          </div>
+        </fieldset>
+
         <!-- Tags -->
         <fieldset>
           <legend class="cd-header"
     </button>
   </div>
 </ng-template>
+
+<ng-template #loadingTpl>
+  <div class="cd-col-form-input">
+    <cd-loading-panel i18n>Checking multi-site status...</cd-loading-panel>
+  </div>
+</ng-template>
index 44318eda88e165b9a26b21b73e12c94b141726a2..34619824f206839b307287d01b94439481b3552a 100644 (file)
@@ -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');
+    });
+  });
 });
index 836ab3d301b3f23a17961916549c4e00e7dd3594..59f6dd6e3995db9c3b03099149b3c8ba4a82b2d8 100644 (file)
@@ -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<any>;
+  isDefaultZoneGroup$: Observable<boolean>;
 
   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(
           () => {
index bdb4decd9dab36e7c8c350688211445ca018c8e1..4936ee54a4848b2314e08ea8a3f506807e6e70e3 100644 (file)
@@ -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
index 4f024f25f41c816da629419b2520656e9918251c..36cafa855a3f2a392b2e464fff88e9ff92833250 100644 (file)
@@ -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
index 04755928b0a91edd030bc52199f4a87351b205e8..248a59292d7257eea117354f99e7adf961a28388 100644 (file)
@@ -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,
index eaed2c4abac2fd96a4018dba98b02f5c563be85c..f930af6cba3cf7b871c45585fd3441c8aa66727d 100644 (file)
@@ -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');
   });
index ddeeadf5e49baee39fb3ca085e76047af4452261..8c9a9bacf4d4400050a42bf3d1890043ff41c026 100644 (file)
@@ -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')
       };
 
index da789d29fc4959cd5c53cb8be244c06b87076d26..9081c21e440035c5d06fccf20ad8d95f505891d7 100644 (file)
@@ -30,4 +30,8 @@ export class RgwMultisiteService {
   getSyncStatus() {
     return this.http.get(`${this.url}/sync_status`);
   }
+
+  status() {
+    return this.http.get(`${this.uiUrl}/status`);
+  }
 }
index dd907e1dc7f53d75e2881a098521596bf1097add..d98613b53d62746ee44789f05dc2e21789e915a4 100644 (file)
@@ -10615,6 +10615,9 @@ paths:
                   type: string
                 placement_target:
                   type: string
+                replication:
+                  default: 'false'
+                  type: string
                 tags:
                   type: string
                 uid:
index fb6c83d60cbe437a25cabd95ecc76c81b69a133e..0d7df8e31e879f93b8180a33ab0d2e958be36f16 100644 (file)
@@ -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
index b84838d10167a39f771b469c20c2de988d06e724..6e7a2960644aac8c2c6fc434cda744942d91ccf0 100644 (file)
@@ -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,