]> 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... sync-policy-replication
authorNaman Munet <naman.munet@ibm.com>
Wed, 18 Mar 2026 08:18:06 +0000 (13:48 +0530)
committerNaman Munet <naman.munet@ibm.com>
Thu, 9 Apr 2026 12:00:27 +0000 (17:30 +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/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/services/rgw_client.py

index ec1ed24d540f6d725aae043b750125e3a7392863..a41c20c79f2b0325079553858c3779155996f4af 100755 (executable)
@@ -508,17 +508,42 @@ 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
+        if replication_config and replication_config.get('Rule'):
+            return replication_config
+
+        # If no S3 replication config exists, check for sync policies
+        try:
+            sync_policy = multisite.get_sync_policy(bucket_name=bucket_name)
+            # Check if there are sync policy groups with 'enabled' status
+            if sync_policy and sync_policy.get('groups'):
+                for group in sync_policy['groups']:
+                    group_status = group.get('status', '').lower()
+                    if group_status == 'enabled':
+                        # A sync policy exists but there is no S3 replication rule.
+                        # The UI should direct the user to the multisite sync-policy
+                        # page rather than showing this as a normal replication rule.
+                        return {'syncPolicyOnly': True}
+        except Exception:
+            pass
+
+        return {'Role': ''}
 
     @staticmethod
     def strip_tenant_from_bucket_name(bucket_name):
index 3c51747db8e1a4b14bd5203b0c04816515c9c173..76e80e548abe849d0e2a1a6c7a9277e9a9f543d9 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>{{ selection.replication | json}}</pre>
+                }
+                </td>
               </tr>
               <tr>
                 <td i18n
index 945502cda93b18a772c780d1f4c126b30223c404..d704bd2fdf21778674b02af626ba7d8c04153a22 100644 (file)
@@ -23,6 +23,7 @@ export class RgwBucketDetailsComponent implements OnChanges {
   lifecycleFormat: 'json' | 'xml' = 'json';
   aclPermissions: Record<string, string[]> = {};
   replicationStatus = $localize`Disabled`;
+  hasSyncPolicyOnly = false;
   bucketRateLimit: RgwRateLimitConfig;
 
   constructor(private rgwBucketService: RgwBucketService, private cd: ChangeDetectorRef) {}
@@ -78,8 +79,13 @@ export class RgwBucketDetailsComponent implements OnChanges {
 
   extraxtDetailsfromResponse() {
     this.aclPermissions = this.parseXmlAcl(this.selection.acl, this.selection.owner);
-    if (this.selection.replication?.['Rule']?.['Status']) {
+    this.hasSyncPolicyOnly = this.selection.replication?.['syncPolicyOnly'] === true;
+    if (this.hasSyncPolicyOnly) {
+      this.replicationStatus = $localize`Enabled`;
+    } else if (this.selection.replication?.['Rule']?.['Status']) {
       this.replicationStatus = this.selection.replication?.['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 5355a9ef18562530c58871c469a826413bf7faac..d790f254df2ca5815980f92ee7a22a6d69e89b58 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,18 @@ 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')
+
     @RestClient.api_post('?Action=CreateTopic&Name={name}')
     def create_topic(self, request=None, name: str = '',
                      daemon_name: str = '',
@@ -2997,6 +3009,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(