]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add option to edit zone with keys/ 68114/head
authorAashish Sharma <aashish@li-e9bf2ecc-2ad7-11b2-a85c-baf05c5182ab.ibm.com>
Tue, 31 Mar 2026 04:30:23 +0000 (10:00 +0530)
committerAashish Sharma <aashish@li-e9bf2ecc-2ad7-11b2-a85c-baf05c5182ab.ibm.com>
Fri, 10 Apr 2026 07:36:53 +0000 (13:06 +0530)
argument like"sync_from" and "sync_from_all"

Currently, there is no option to configure the sync_from and sync_from_all keys directly while creating or editing a zone from the dashboard. These arguments are particularly important when setting up archive zones. In archive zones, duplicate objects appear when sync_from_all is set to true (which is the default). The fix is to:
1.Set sync_from_all to false
2.Set sync_from to point to the master zone only
This ensures that the archive zone syncs exclusively from the master zone, preventing duplicate object issues.

Fixes: https://tracker.ceph.com/issues/75950
Signed-off-by: Aashish Sharma <aasharma@redhat.com>
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts
src/pybind/mgr/dashboard/frontend/src/styles.scss
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rgw_client.py

index 7fef9480043caf97f6ecbce9ac507325303ba8a9..f0d3d0dce4952276a93cd344a768224eb60fda19 100755 (executable)
@@ -1490,11 +1490,13 @@ class RgwZone(RESTController):
     @allow_empty_body
     # pylint: disable=W0613
     def create(self, zone_name, zonegroup_name=None, default=False, master=False,
-               zone_endpoints=None, access_key=None, secret_key=None, tier_type=''):
+               zone_endpoints=None, access_key=None, secret_key=None, tier_type='',
+               sync_from: str = '', sync_from_all: str = ''):
         multisite_instance = RgwMultisite()
         result = multisite_instance.create_zone(zone_name, zonegroup_name, default,
                                                 master, zone_endpoints, access_key,
-                                                secret_key, tier_type)
+                                                secret_key, tier_type, sync_from,
+                                                sync_from_all)
         return result
 
     @allow_empty_body
@@ -1539,13 +1541,14 @@ class RgwZone(RESTController):
             master: str = '', zone_endpoints: str = '', access_key: str = '', secret_key: str = '',
             placement_target: str = '', data_pool: str = '', index_pool: str = '',
             data_extra_pool: str = '', storage_class: str = '', data_pool_class: str = '',
-            compression: str = '', tier_type: str = ''):
+            compression: str = '', tier_type: str = '', sync_from: str = '',
+            sync_from_all: str = ''):
         multisite_instance = RgwMultisite()
         result = multisite_instance.edit_zone(zone_name, new_zone_name, zonegroup_name, default,
                                               master, zone_endpoints, access_key, secret_key,
                                               placement_target, data_pool, index_pool,
                                               data_extra_pool, storage_class, data_pool_class,
-                                              compression, tier_type)
+                                              compression, tier_type, sync_from, sync_from_all)
         return result
 
     @Endpoint()
index 8791b9cfab16de00ed510ceba975d427476312df..72037d4dfe95db5b274eeb152aa2acdf20a597ed 100644 (file)
@@ -49,6 +49,8 @@ export class RgwZone {
   notif_pool: string;
   endpoints: string;
   tier_type: string;
+  sync_from: string;
+  sync_from_all: boolean;
 }
 
 export class SystemKey {
index 093032c6aadc791ed9ee455a544b951e38462ee0..54d117fb56380934b0ff0f1ac5c64f93c1a002ac 100644 (file)
           id="archive_zone"
           formControlName="archive_zone"
           cdOptionalField="Archive"
+          (checkedChange)="onArchiveZoneChange($event)"
           i18n
           >Archive
           <cd-help-text>
           </cd-help-text>
         </cds-checkbox>
       </div>
+      <div class="form-item">
+        <cds-checkbox
+          id="sync_from_all"
+          formControlName="sync_from_all"
+          cdOptionalField="Sync from all"
+          (checkedChange)="onZoneGroupChange(multisiteZoneForm.getValue('selectedZonegroup'))"
+          i18n
+          >Sync from all zones
+          <cd-help-text>
+            <span
+              >Enable this option to synchronize data from all zones.</span
+            >
+          </cd-help-text>
+          <div class="cds-mt-3">
+            <cd-alert-panel *ngIf="multisiteZoneForm.get('archive_zone')?.value === true"
+                            type="warning">
+                “Sync from all zones” is disabled to avoid duplicate objects in archive zones. Please select specific zones instead.
+            </cd-alert-panel>
+          </div>
+        </cds-checkbox>
+      </div>
+      <div *ngIf="multisiteZoneForm.get('sync_from_all')?.value === false"
+           class="form-item">
+        <cds-combo-box type="multi"
+                       selectionFeedback="top-after-reopen"
+                       label="Sync from zones"
+                       formControlName="sync_from_zones"
+                       id="sync_from_zones"
+                       [appendInline]="true"
+                       [items]="syncFromZonesOptions"
+                       i18n>
+          <cds-dropdown-list></cds-dropdown-list>
+        </cds-combo-box>
+        <cd-help-text>
+          <span i18n>Select specific zones to synchronize data from when "Sync from all zones" is disabled.</span>
+        </cd-help-text>
+      </div>
       <div class="form-item">
         <cds-text-label
           i18n
index 0c434d3540056916c9c02138a7027395aca11f75..3bbf271fb8f6dc149c71924a22b26fac8cbc1a99 100644 (file)
@@ -14,6 +14,7 @@ import { NotificationService } from '~/app/shared/services/notification.service'
 import { RgwRealm, RgwZone, RgwZonegroup, SystemKey } from '../models/rgw-multisite';
 import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
 import { CdForm } from '~/app/shared/forms/cd-form';
+import { ComboBoxItem } from '~/app/shared/models/combo-box.model';
 
 @Component({
   selector: 'cd-rgw-multisite-zone-form',
@@ -42,7 +43,7 @@ export class RgwMultisiteZoneFormComponent extends CdForm implements OnInit {
   syncStatusTimedOut: boolean = false;
   bsModalRef: NgbModalRef;
   createSystemUser: boolean = false;
-  master_zone_of_master_zonegroup: RgwZone;
+  master_zone_name: string;
   masterZoneUser: any;
   access_key: any;
   master_zonegroup_of_realm: RgwZonegroup;
@@ -52,6 +53,8 @@ export class RgwMultisiteZoneFormComponent extends CdForm implements OnInit {
     'To see or copy your S3 secret key, go to Object Gateway > Users and click on your user name. In Keys, click Show. View the secret key by clicking Show and copy the key by clicking Copy to Clipboard';
   AccessKeyText =
     'To see or copy your S3 access key, go to Object Gateway > Users and click on your user name. In Keys, click Show. View the access key by clicking Show and copy the key by clicking Copy to Clipboard';
+  syncFromZonesOptions: ComboBoxItem[] = [];
+
   constructor(
     public activeModal: NgbActiveModal,
     public actionLabels: ActionLabelsI18n,
@@ -100,14 +103,33 @@ export class RgwMultisiteZoneFormComponent extends CdForm implements OnInit {
       placementDataExtraPool: new UntypedFormControl(null),
       storageClass: new UntypedFormControl(null),
       storageDataPool: new UntypedFormControl(null),
-      storageCompression: new UntypedFormControl(null)
+      storageCompression: new UntypedFormControl(null),
+      sync_from_zones: new UntypedFormControl(null),
+      sync_from_all: new UntypedFormControl(true)
     });
   }
 
   onZoneGroupChange(zonegroupName: string) {
+    this.syncFromZonesOptions = [];
+
     let zg = new RgwZonegroup();
     zg.name = zonegroupName;
+
     this.rgwZoneGroupService.get(zg).subscribe((zonegroup: RgwZonegroup) => {
+      const syncFromZones = this.info?.data?.info?.sync_from || [];
+
+      this.syncFromZonesOptions = zonegroup.zones
+        .filter((zone) => zone.name !== this.multisiteZoneForm.getValue('zoneName'))
+        .map((zone) => ({
+          name: zone.name,
+          content: zone.name,
+          selected: syncFromZones.includes(zone.name)
+        }));
+
+      this.multisiteZoneForm
+        .get('sync_from_zones')
+        .setValue(this.syncFromZonesOptions.filter((opt) => opt.selected));
+
       if (_.isEmpty(zonegroup.master_zone)) {
         this.multisiteZoneForm.get('master_zone').setValue(true);
         this.multisiteZoneForm.get('master_zone').disable();
@@ -117,6 +139,10 @@ export class RgwMultisiteZoneFormComponent extends CdForm implements OnInit {
         this.multisiteZoneForm.get('master_zone').disable();
         this.disableMaster = true;
       }
+      const masterZone = this.zoneList.find((zone) => zone.id === zonegroup.master_zone);
+      if (masterZone) {
+        this.master_zone_name = masterZone.name;
+      }
     });
     if (
       this.multisiteZoneForm.getValue('selectedZonegroup') !==
@@ -166,6 +192,8 @@ export class RgwMultisiteZoneFormComponent extends CdForm implements OnInit {
       this.multisiteZoneForm
         .get('archive_zone')
         .setValue(this.info.data.info.tier_type === 'archive');
+      this.onArchiveZoneChange(this.info.data.info.tier_type === 'archive');
+      this.multisiteZoneForm.get('sync_from_all').setValue(this.info.data.info.sync_from_all);
       this.multisiteZoneForm
         .get('placementTarget')
         .setValue((this.info.data?.parentNode || this.info.parent.data)?.default_placement);
@@ -245,6 +273,10 @@ export class RgwMultisiteZoneFormComponent extends CdForm implements OnInit {
       this.zone.system_key.access_key = values['access_key'];
       this.zone.system_key.secret_key = values['secret_key'];
       this.zone.tier_type = values['archive_zone'] ? 'archive' : '';
+      this.zone.sync_from = values['sync_from_zones']
+        ? values['sync_from_zones'].map((zone: RgwZone) => zone.name).join(',')
+        : '';
+      this.zone.sync_from_all = values['sync_from_all'];
       this.rgwZoneService
         .create(
           this.zone,
@@ -275,6 +307,10 @@ export class RgwMultisiteZoneFormComponent extends CdForm implements OnInit {
       this.zone.system_key.access_key = values['access_key'];
       this.zone.system_key.secret_key = values['secret_key'];
       this.zone.tier_type = values['archive_zone'] ? 'archive' : '';
+      this.zone.sync_from = values['sync_from_zones']
+        ? values['sync_from_zones'].map((zone: RgwZone) => zone.name).join(',')
+        : '';
+      this.zone.sync_from_all = values['sync_from_all'];
       this.rgwZoneService
         .update(
           this.zone,
@@ -305,4 +341,21 @@ export class RgwMultisiteZoneFormComponent extends CdForm implements OnInit {
         );
     }
   }
+
+  onArchiveZoneChange(isArchiveZone: boolean) {
+    if (isArchiveZone) {
+      this.multisiteZoneForm.get('sync_from_all').setValue(false);
+      this.multisiteZoneForm.get('sync_from_all').disable();
+      this.syncFromZonesOptions = this.syncFromZonesOptions.map((zone) => ({
+        ...zone,
+        selected: zone.name === this.master_zone_name
+      }));
+      this.multisiteZoneForm
+        .get('sync_from_zones')
+        .setValue(this.syncFromZonesOptions.filter((opt) => opt.selected));
+    } else {
+      this.multisiteZoneForm.get('sync_from_all').enable();
+      this.multisiteZoneForm.get('sync_from_all').setValue(true);
+    }
+  }
 }
index 876a18dfd7d3d20284b2efb9a4eac945758ae424..35007f37939ffff349db32dccb5938f3116e3e1c 100644 (file)
@@ -34,7 +34,9 @@ export class RgwZoneService {
       zone_endpoints: endpoints,
       access_key: zone.system_key.access_key,
       secret_key: zone.system_key.secret_key,
-      tier_type: zone.tier_type
+      tier_type: zone.tier_type,
+      sync_from: zone.sync_from,
+      sync_from_all: zone.sync_from_all
     });
     return this.http.post(`${this.url}`, null, { params: params });
   }
@@ -100,7 +102,9 @@ export class RgwZoneService {
       storage_class: storageClass,
       data_pool_class: dataPoolClass,
       compression: compression,
-      tier_type: zone.tier_type
+      tier_type: zone.tier_type,
+      sync_from: zone.sync_from,
+      sync_from_all: zone.sync_from_all
     };
     return this.http.put(`${this.url}/${zone.name}`, requestBody);
   }
@@ -128,6 +132,9 @@ export class RgwZoneService {
     nodes['endpoints'] = zone.endpoints;
     nodes['is_master'] = zonegroup && zonegroup.master_zone === zone.id ? true : false;
     nodes['tier_type'] = zonegroup?.zones?.find((z) => z.name === zone.name)?.tier_type || '';
+    nodes['sync_from'] = zonegroup?.zones?.find((z) => z.name === zone.name)?.sync_from || [];
+    nodes['sync_from_all'] =
+      zonegroup?.zones?.find((z) => z.name === zone.name)?.sync_from_all ?? true;
     nodes['type'] = 'zone';
     const zoneNames = zones.map((zone: RgwZone) => {
       return zone['name'];
index fcd7d2c68cea6691d7adecfe064a24f8557ab2b7..9c25a83972af159e9f36ea129cac04a92e141a79 100644 (file)
@@ -243,3 +243,7 @@ input:-webkit-autofill:active {
 .border-subtle-inline-end {
   border-inline-end: 1px solid var(--cds-border-subtle-01);
 }
+
+.popover {
+  --bs-popover-zindex: 9999;
+}
index 5f6024ca9de3d9767bc80f87dabc17a8634a7875..d1fa12f826a9bb3a972c38107a987838e4551e20 100644 (file)
@@ -20741,6 +20741,12 @@ paths:
                   type: boolean
                 secret_key:
                   type: string
+                sync_from:
+                  default: ''
+                  type: string
+                sync_from_all:
+                  default: ''
+                  type: string
                 tier_type:
                   default: ''
                   type: string
@@ -21171,6 +21177,12 @@ paths:
                 storage_class:
                   default: ''
                   type: string
+                sync_from:
+                  default: ''
+                  type: string
+                sync_from_all:
+                  default: ''
+                  type: string
                 tier_type:
                   default: ''
                   type: string
index 11f63e14b268dbe3092624717c17c736b4e4437e..881cb0c63723d9236b369c43acb300f3811c136d 100755 (executable)
@@ -2391,7 +2391,8 @@ class RgwMultisite:
             raise DashboardException(error, http_status_code=500, component='rgw')
 
     def create_zone(self, zone_name, zonegroup_name, default, master, endpoints, access_key,
-                    secret_key, tier_type):
+                    secret_key, tier_type, sync_from: Optional[str] = None,
+                    sync_from_all: Optional[str] = None):
         rgw_zone_create_cmd = ['zone', 'create']
         cmd_create_zone_options = ['--rgw-zone', zone_name]
         if zonegroup_name != 'null':
@@ -2413,6 +2414,12 @@ class RgwMultisite:
         if tier_type:
             cmd_create_zone_options.append('--tier-type')
             cmd_create_zone_options.append(tier_type)
+        if sync_from is not None:
+            cmd_create_zone_options.append('--sync-from')
+            cmd_create_zone_options.append(sync_from)
+        if sync_from_all is not None:
+            cmd_create_zone_options.append('--sync-from-all')
+            cmd_create_zone_options.append(str(sync_from_all).lower())
         rgw_zone_create_cmd += cmd_create_zone_options
         try:
             exit_code, out, err = mgr.send_rgwadmin_command(rgw_zone_create_cmd)
@@ -2434,7 +2441,8 @@ class RgwMultisite:
         return '', ''
 
     def modify_zone(self, zone_name: str, zonegroup_name: str, default: str, master: str,
-                    endpoints: str, access_key: str, secret_key: str, tier_type: str):
+                    endpoints: str, access_key: str, secret_key: str, tier_type: str,
+                    sync_from: Optional[str] = None, sync_from_all: Optional[str] = None):
         rgw_zone_modify_cmd = ['zone', 'modify', '--rgw-zonegroup',
                                zonegroup_name, '--rgw-zone', zone_name]
         if endpoints:
@@ -2453,6 +2461,12 @@ class RgwMultisite:
         if tier_type is not None:
             rgw_zone_modify_cmd.append('--tier-type')
             rgw_zone_modify_cmd.append(tier_type)
+        if sync_from is not None:
+            rgw_zone_modify_cmd.append('--sync-from')
+            rgw_zone_modify_cmd.append(sync_from)
+        if sync_from_all is not None:
+            rgw_zone_modify_cmd.append('--sync-from-all')
+            rgw_zone_modify_cmd.append(str(sync_from_all).lower())
         try:
             exit_code, _, err = mgr.send_rgwadmin_command(rgw_zone_modify_cmd)
             if exit_code > 0:
@@ -2528,7 +2542,8 @@ class RgwMultisite:
                   master: str = '', endpoints: str = '', access_key: str = '', secret_key: str = '',
                   placement_target: str = '', data_pool: str = '', index_pool: str = '',
                   data_extra_pool: str = '', storage_class: str = '', data_pool_class: str = '',
-                  compression: str = '', tier_type: str = ''):
+                  compression: str = '', tier_type: str = '', sync_from: str = '',
+                  sync_from_all: str = ''):
         if new_zone_name != zone_name:
             rgw_zone_rename_cmd = ['zone', 'rename', '--rgw-zone',
                                    zone_name, '--zone-new-name', new_zone_name]
@@ -2542,7 +2557,7 @@ class RgwMultisite:
                 raise DashboardException(error, http_status_code=500, component='rgw')
             self.update_period()
         self.modify_zone(new_zone_name, zonegroup_name, default, master, endpoints, access_key,
-                         secret_key, tier_type)
+                         secret_key, tier_type, sync_from, sync_from_all)
 
         if placement_target:
             self.add_placement_targets_storage_class_zone(