]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: sync policy created for a bucket in Object >> Multi-site >> Sync-polic... 67869/head
authorNaman Munet <naman.munet@ibm.com>
Wed, 18 Mar 2026 08:18:06 +0000 (13:48 +0530)
committerNaman Munet <naman.munet@ibm.com>
Fri, 17 Apr 2026 08:01:58 +0000 (13:31 +0530)
Fixes: https://tracker.ceph.com/issues/75581
Signed-off-by: Naman Munet <naman.munet@ibm.com>
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-replication.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-bucket-details/rgw-bucket-details.component.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.ts
src/pybind/mgr/dashboard/services/rgw_client.py

index ec1ed24d540f6d725aae043b750125e3a7392863..39a71ff6ca9fbb55c324500e67aeaa643d0b12bc 100755 (executable)
@@ -508,17 +508,41 @@ class RgwBucket(RgwRESTController):
             return None
 
         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)
+        if replication:
+            zonegroup_name = RgwClient.admin_instance(
+                daemon_name=daemon_name).get_default_zonegroup()
+            policy_exists = multisite.policy_group_exists(_SYNC_GROUP_ID, zonegroup_name)
+            if not policy_exists:
+                multisite.create_dashboard_admin_sync_group(zonegroup_name=zonegroup_name)
+            return rgw_client.set_bucket_replication(bucket_name)
 
-        return rgw_client.set_bucket_replication(bucket_name, replication)
+        return rgw_client.delete_bucket_replication(bucket_name)
 
     def _get_replication(self, bucket_name: str, owner, daemon_name):
+        multisite = RgwMultisite()
         rgw_client = RgwClient.instance(owner, daemon_name)
-        return rgw_client.get_bucket_replication(bucket_name)
+        replication_config = rgw_client.get_bucket_replication(bucket_name)
+
+        # Check if there's a valid S3 replication config with actual rules
+        replication_rules_configured = bool(replication_config and replication_config.get('Rule'))
+
+        # Check for sync policies
+        sync_policy_active = False
+        try:
+            sync_policy = multisite.get_sync_policy(bucket_name=bucket_name)
+            # Check if there are sync policy groups with 'enabled' status
+            groups = sync_policy.get('groups', [])
+            sync_policy_active = any(g.get('status', '').lower() == 'enabled' for g in groups)
+        except (RequestException, DashboardException):
+            # Silently ignore errors from sync policy check
+            pass
+
+        return {
+            'sync_policy_active': sync_policy_active,
+            'replication_rules_configured': replication_rules_configured,
+            'policy': replication_config if replication_config else {}
+        }
 
     @staticmethod
     def strip_tenant_from_bucket_name(bucket_name):
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-replication.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-bucket-replication.ts
new file mode 100644 (file)
index 0000000..e610c7a
--- /dev/null
@@ -0,0 +1,5 @@
+export interface RgwBucketReplication {
+  sync_policy_active: boolean;
+  replication_rules_configured: boolean;
+  policy: any;
+}
index 3c51747db8e1a4b14bd5203b0c04816515c9c173..817088ad4b840999188509157bb4b5176ebb9d9d 100644 (file)
               <tr>
                 <td i18n
                     class="bold w-25">Replication policy</td>
-                <td><pre>{{ selection.replication | json}}</pre></td>
+                <td>
+                @if(hasSyncPolicyOnly) {
+                Sync policy configured. View on
+                <a routerLink="/rgw/multisite/sync-policy"
+                   i18n>Sync-policy page.</a>
+                } @else {
+                  <pre>{{ replicationData?.policy | json}}</pre>
+                }
+                </td>
               </tr>
               <tr>
                 <td i18n
index 945502cda93b18a772c780d1f4c126b30223c404..a138b9d91a6f1b86b241b9250e679006634cb128 100644 (file)
@@ -4,6 +4,7 @@ import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
 
 import * as xml2js from 'xml2js';
 import { RgwRateLimitConfig } from '../models/rgw-rate-limit';
+import { RgwBucketReplication } from '../models/rgw-bucket-replication';
 
 @Component({
   selector: 'cd-rgw-bucket-details',
@@ -23,6 +24,8 @@ export class RgwBucketDetailsComponent implements OnChanges {
   lifecycleFormat: 'json' | 'xml' = 'json';
   aclPermissions: Record<string, string[]> = {};
   replicationStatus = $localize`Disabled`;
+  hasSyncPolicyOnly = false;
+  replicationData: RgwBucketReplication;
   bucketRateLimit: RgwRateLimitConfig;
 
   constructor(private rgwBucketService: RgwBucketService, private cd: ChangeDetectorRef) {}
@@ -78,8 +81,26 @@ export class RgwBucketDetailsComponent implements OnChanges {
 
   extraxtDetailsfromResponse() {
     this.aclPermissions = this.parseXmlAcl(this.selection.acl, this.selection.owner);
-    if (this.selection.replication?.['Rule']?.['Status']) {
-      this.replicationStatus = this.selection.replication?.['Rule']?.['Status'];
+
+    this.replicationData = {
+      sync_policy_active: this.selection.replication?.sync_policy_active === true,
+      replication_rules_configured:
+        this.selection.replication?.replication_rules_configured === true,
+      policy: this.selection.replication?.policy || {}
+    };
+
+    this.hasSyncPolicyOnly =
+      this.replicationData.sync_policy_active && !this.replicationData.replication_rules_configured;
+
+    if (this.replicationData.sync_policy_active) {
+      this.replicationStatus = $localize`Enabled`;
+    } else if (
+      this.replicationData.replication_rules_configured &&
+      this.replicationData.policy['Rule']?.['Status']
+    ) {
+      this.replicationStatus = this.replicationData.policy['Rule']['Status'];
+    } else {
+      this.replicationStatus = $localize`Disabled`;
     }
     this.rgwBucketService.getBucketRateLimit(this.selection.bid).subscribe((resp: any) => {
       if (resp && resp.bucket_ratelimit !== undefined) {
index 4106254cae84788e41579b86c4a0aecc58f0d548..59eb8e37402141e046ecddd0c433f9886eba0624 100644 (file)
       <div class="form-item">
         <legend class="cd-header"
                 i18n>Replication</legend>
-        <ng-container *ngIf="{status: multisiteStatus$, isDefaultZg: isDefaultZoneGroup$ | async} as multisiteStatus; else loadingTpl">
+        <ng-container *ngIf="{status: multisiteStatus$ | async, isDefaultZg: isDefaultZoneGroup$ | async} as multisiteStatus; else loadingTpl">
           <cds-checkbox name="replication"
                         formControlName="replication"
                         id="replication"
-                        [disabled]="!multisiteStatus.isDefaultZg && !multisiteStatus.status.available"
+                        [disabled]="!multisiteStatus.isDefaultZg && !multisiteStatus?.status?.available"
+                        [title]="!multisiteStatus.isDefaultZg && !multisiteStatus?.status?.available ? 'Please enable the multisite configuration' : ''"
+                        i18n-title
                         i18n-helperText
                         i18n>Enable
             <cd-help-text>Enables replication for the objects in the bucket.</cd-help-text>
index 512f0462c19054d3540c738b3de0d9d8a269c2ec..c9b7eaa8795724f0c88d750509b55263271ebebf 100644 (file)
@@ -327,11 +327,12 @@ export class RgwBucketFormComponent extends CdForm implements OnInit, AfterViewC
             }
             if (value['replication']) {
               const replicationConfig = value['replication'];
-              if (replicationConfig?.['Rule']?.['Status'] === 'Enabled') {
-                this.bucketForm.get('replication').setValue(true);
-              } else {
-                this.bucketForm.get('replication').setValue(false);
-              }
+              // Only consider S3 replication rules, not sync policies (managed on sync-policy page)
+              const hasReplicationRules =
+                replicationConfig?.['replication_rules_configured'] === true &&
+                replicationConfig?.['policy']?.['Rule']?.['Status'] === 'Enabled';
+
+              this.bucketForm.get('replication').setValue(hasReplicationRules);
             }
             this.filterAclPermissions();
           }
index 5355a9ef18562530c58871c469a826413bf7faac..3482fe935b399731a6bc13fb72faa3f9b819abc4 100755 (executable)
@@ -1114,7 +1114,7 @@ class RgwClient(RestClient):
         return retention_period_days, retention_period_years
 
     @RestClient.api_put('/{bucket_name}?replication')
-    def set_bucket_replication(self, bucket_name, replication: bool, request=None):
+    def set_bucket_replication(self, bucket_name, request=None):
         # pGenerate the minimum replication configuration
         # required for enabling the replication
         root = ET.Element('ReplicationConfiguration',
@@ -1127,7 +1127,7 @@ class RgwClient(RestClient):
         rule_id.text = _SYNC_PIPE_ID
 
         status = ET.SubElement(rule, 'Status')
-        status.text = 'Enabled' if replication else 'Disabled'
+        status.text = 'Enabled'
 
         filter_elem = ET.SubElement(rule, 'Filter')
         prefix = ET.SubElement(filter_elem, 'Prefix')
@@ -1158,6 +1158,19 @@ class RgwClient(RestClient):
                     return None
             raise e
 
+    @RestClient.api_delete('/{bucket_name}?replication')
+    def delete_bucket_replication(self, bucket_name, request=None):
+        # pylint: disable=unused-argument
+        try:
+            request()
+        except RequestException as e:
+            if e.content:
+                content = json_str_to_object(e.content)
+                if content.get('Code') == 'ReplicationConfigurationNotFoundError':
+                    return None
+            raise DashboardException(msg=str(e), component='rgw')
+        return None
+
     @RestClient.api_post('?Action=CreateTopic&Name={name}')
     def create_topic(self, request=None, name: str = '',
                      daemon_name: str = '',
@@ -2997,6 +3010,31 @@ class RgwMultisite:
         # period update --commit
         self.update_period()
 
+    @staticmethod
+    def _is_not_found_sync_policy_error(error: DashboardException) -> bool:
+        message = str(error).lower()
+        not_found_markers = ['not found', 'no such', 'does not exist']
+        return any(marker in message for marker in not_found_markers)
+
+    def remove_dashboard_admin_sync_group(self, zonegroup_name: str = ''):
+        if not self.policy_group_exists(_SYNC_GROUP_ID, zonegroup_name):
+            return
+
+        try:
+            self.remove_sync_pipe(_SYNC_GROUP_ID, _SYNC_PIPE_ID)
+        except DashboardException as error:
+            if not self._is_not_found_sync_policy_error(error):
+                raise
+
+        try:
+            self.remove_sync_flow(_SYNC_GROUP_ID, _SYNC_FLOW_ID,
+                                  SyncFlowTypes.symmetrical.value)
+        except DashboardException as error:
+            if not self._is_not_found_sync_policy_error(error):
+                raise
+
+        self.remove_sync_policy_group(_SYNC_GROUP_ID, update_period=True)
+
     def policy_group_exists(self, group_name: str, zonegroup_name: str):
         try:
             _ = self.get_sync_policy_group(