From: Aashish Sharma Date: Tue, 31 Mar 2026 04:30:23 +0000 (+0530) Subject: mgr/dashboard: Add option to edit zone with keys/ X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=50a1bc6fe8ada3554f6b3b0a9f8a6823ca717d09;p=ceph.git mgr/dashboard: Add option to edit zone with keys/ 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 --- diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index 7fef9480043c..f0d3d0dce495 100755 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -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() diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts index 8791b9cfab16..72037d4dfe95 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts @@ -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 { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html index 093032c6aadc..54d117fb5638 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-zone-form/rgw-multisite-zone-form.component.html @@ -112,6 +112,7 @@ id="archive_zone" formControlName="archive_zone" cdOptionalField="Archive" + (checkedChange)="onArchiveZoneChange($event)" i18n >Archive @@ -121,6 +122,43 @@ +
+ Sync from all zones + + Enable this option to synchronize data from all zones. + +
+ + “Sync from all zones” is disabled to avoid duplicate objects in archive zones. Please select specific zones instead. + +
+
+
+
+ + + + + Select specific zones to synchronize data from when "Sync from all zones" is disabled. + +
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); + } + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts index 876a18dfd7d3..35007f37939f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-zone.service.ts @@ -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']; diff --git a/src/pybind/mgr/dashboard/frontend/src/styles.scss b/src/pybind/mgr/dashboard/frontend/src/styles.scss index fcd7d2c68cea..9c25a83972af 100644 --- a/src/pybind/mgr/dashboard/frontend/src/styles.scss +++ b/src/pybind/mgr/dashboard/frontend/src/styles.scss @@ -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; +} diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 5f6024ca9de3..d1fa12f826a9 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -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 diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py index 11f63e14b268..881cb0c63723 100755 --- a/src/pybind/mgr/dashboard/services/rgw_client.py +++ b/src/pybind/mgr/dashboard/services/rgw_client.py @@ -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(