From: Naman Munet Date: Mon, 4 May 2026 12:57:53 +0000 (+0530) Subject: mgr/dashboard: multisite sync-policy page should include daemon selection X-Git-Tag: v21.0.1~63^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=refs%2Fpull%2F68734%2Fhead;p=ceph.git mgr/dashboard: multisite sync-policy page should include daemon selection Fixes: https://tracker.ceph.com/issues/71522 Changes includes: - Added daemon selection support to all sync policy endpoints - Enhanced backend with daemon context awareness - Fetch only the sync policies from the specified daemon Signed-off-by: Naman Munet --- diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index 63a419912aa..9e06597f171 100755 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -185,60 +185,76 @@ class RgwMultisiteController(RESTController): @Endpoint(path='/sync-policy') @EndpointDoc("Get the sync policy") @ReadPermission - def get_sync_policy(self, bucket_name='', zonegroup_name='', all_policy=None): + def get_sync_policy(self, bucket_name='', zonegroup_name='', all_policy=None, daemon_name=''): multisite_instance = RgwMultisite() all_policy = str_to_bool(all_policy) if all_policy: sync_policy_list = [] - buckets = json.loads(RgwBucket().list(stats=False)) - zonegroups_info = RgwMultisite().get_all_zonegroups_info() - default_zonegroup = '' - if 'zonegroups' in zonegroups_info and 'default_zonegroup' in zonegroups_info: - default_zonegroup = next( - (zonegroup['name'] for zonegroup in zonegroups_info['zonegroups'] - if 'id' in zonegroup and 'name' in zonegroup - and zonegroup['id'] == zonegroups_info['default_zonegroup']), - '' - ) + # Filter buckets by daemon_name to only get buckets from that daemon's zone/zonegroup + buckets = json.loads(RgwBucket().list( + stats=False, daemon_name=daemon_name if daemon_name else None)) + # Get zonegroup from daemon if daemon_name is provided, otherwise use default + target_zonegroup = '' + if daemon_name: + target_zonegroup = ( + multisite_instance.get_zonegroup_from_daemon(daemon_name) or '') + if not target_zonegroup: + zonegroups_info = RgwMultisite().get_all_zonegroups_info() + if 'zonegroups' in zonegroups_info and 'default_zonegroup' in zonegroups_info: + target_zonegroup = next( + (zonegroup['name'] for zonegroup in zonegroups_info['zonegroups'] + if 'id' in zonegroup and 'name' in zonegroup + and zonegroup['id'] == zonegroups_info['default_zonegroup']), + '' + ) for bucket in buckets: - sync_policy = multisite_instance.get_sync_policy(bucket, zonegroup_name) - for policy in sync_policy['groups']: - policy['bucketName'] = bucket - sync_policy_list.append(policy) - other_sync_policy = multisite_instance.get_sync_policy(bucket_name, zonegroup_name) + try: + sync_policy = multisite_instance.get_sync_policy( + bucket, zonegroup_name, daemon_name) + for policy in sync_policy['groups']: + policy['bucketName'] = bucket + sync_policy_list.append(policy) + except DashboardException: + # Skip buckets that don't have sync policies or aren't accessible + logger.debug("Skipping bucket %s - no sync policy or not accessible", bucket) + continue + other_sync_policy = multisite_instance.get_sync_policy( + bucket_name, zonegroup_name, daemon_name) for policy in other_sync_policy['groups']: - policy['zonegroup'] = default_zonegroup + policy['zonegroup'] = target_zonegroup sync_policy_list.append(policy) return sync_policy_list - return multisite_instance.get_sync_policy(bucket_name, zonegroup_name) + return multisite_instance.get_sync_policy(bucket_name, zonegroup_name, daemon_name) @Endpoint(path='/sync-policy-group') @EndpointDoc("Get the sync policy group") @ReadPermission - def get_sync_policy_group(self, group_id: str, bucket_name=''): + def get_sync_policy_group(self, group_id: str, bucket_name='', daemon_name=''): multisite_instance = RgwMultisite() - return multisite_instance.get_sync_policy_group(group_id, bucket_name) + return multisite_instance.get_sync_policy_group(group_id, bucket_name, '', daemon_name) @Endpoint(method='POST', path='/sync-policy-group') @EndpointDoc("Create the sync policy group") @CreatePermission - def create_sync_policy_group(self, group_id: str, status: str, bucket_name=''): + def create_sync_policy_group(self, group_id: str, status: str, bucket_name='', daemon_name=''): multisite_instance = RgwMultisite() - return multisite_instance.create_sync_policy_group(group_id, status, bucket_name, True) + return multisite_instance.create_sync_policy_group( + group_id, status, bucket_name, True, daemon_name) @Endpoint(method='PUT', path='/sync-policy-group') @EndpointDoc("Update the sync policy group") @UpdatePermission - def update_sync_policy_group(self, group_id: str, status: str, bucket_name=''): + def update_sync_policy_group(self, group_id: str, status: str, bucket_name='', daemon_name=''): multisite_instance = RgwMultisite() - return multisite_instance.update_sync_policy_group(group_id, status, bucket_name, True) + return multisite_instance.update_sync_policy_group( + group_id, status, bucket_name, True, daemon_name) @Endpoint(method='DELETE', path='/sync-policy-group') @EndpointDoc("Remove the sync policy group") @DeletePermission - def remove_sync_policy_group(self, group_id: str, bucket_name=''): + def remove_sync_policy_group(self, group_id: str, bucket_name='', daemon_name=''): multisite_instance = RgwMultisite() - return multisite_instance.remove_sync_policy_group(group_id, bucket_name, True) + return multisite_instance.remove_sync_policy_group(group_id, bucket_name, True, daemon_name) @Endpoint(method='PUT', path='/sync-flow') @EndpointDoc("Create or update the sync flow") @@ -247,20 +263,22 @@ class RgwMultisiteController(RESTController): source_zone: Optional[str] = None, destination_zone: Optional[str] = None, zones: Optional[Dict[str, List]] = None, - bucket_name=''): + bucket_name='', daemon_name=''): multisite_instance = RgwMultisite() - return multisite_instance.create_sync_flow(group_id, flow_id, flow_type, zones, - bucket_name, source_zone, destination_zone, True) + return multisite_instance.create_sync_flow( + group_id, flow_id, flow_type, zones, bucket_name, source_zone, + destination_zone, True, daemon_name) @Endpoint(method='DELETE', path='/sync-flow') @EndpointDoc("Remove the sync flow") @DeletePermission def remove_sync_flow(self, flow_id: str, flow_type: str, group_id: str, source_zone='', destination_zone='', zones: Optional[List[str]] = None, - bucket_name=''): + bucket_name='', daemon_name=''): multisite_instance = RgwMultisite() - return multisite_instance.remove_sync_flow(group_id, flow_id, flow_type, source_zone, - destination_zone, zones, bucket_name, True) + return multisite_instance.remove_sync_flow( + group_id, flow_id, flow_type, source_zone, destination_zone, + zones, bucket_name, True, daemon_name) @Endpoint(method='PUT', path='/sync-pipe') @EndpointDoc("Create or update the sync pipe") @@ -270,12 +288,12 @@ class RgwMultisiteController(RESTController): destination_zones: Dict[str, Any], source_bucket: str = '', destination_bucket: str = '', bucket_name: str = '', - user: str = '', mode: str = ''): + user: str = '', mode: str = '', daemon_name=''): multisite_instance = RgwMultisite() return multisite_instance.create_sync_pipe(group_id, pipe_id, source_zones, destination_zones, source_bucket, destination_bucket, bucket_name, True, - user, mode) + user, mode, daemon_name) @Endpoint(method='DELETE', path='/sync-pipe') @EndpointDoc("Remove the sync pipe") @@ -283,10 +301,11 @@ class RgwMultisiteController(RESTController): def remove_sync_pipe(self, group_id: str, pipe_id: str, source_zones: Optional[List[str]] = None, destination_zones: Optional[List[str]] = None, - bucket_name: str = ''): + bucket_name: str = '', daemon_name=''): multisite_instance = RgwMultisite() - return multisite_instance.remove_sync_pipe(group_id, pipe_id, source_zones, - destination_zones, bucket_name, True) + return multisite_instance.remove_sync_pipe( + group_id, pipe_id, source_zones, destination_zones, + bucket_name, True, daemon_name) @APIRouter('/rgw/daemon', Scope.RGW) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.ts index c5a45686226..e652a923d7c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/context/context.component.ts @@ -28,13 +28,15 @@ export class ContextComponent implements OnInit, OnDestroy { private rgwRoleUrlPrefix = '/rgw/roles'; private rgwBuckerUrlPrefix = '/rgw/bucket'; private rgwAccountsUrlPrefix = '/rgw/accounts'; + private rgwMultisiteSyncPolicyPrefix = '/rgw/multisite/sync-policy'; permissions: Permissions; featureToggleMap$: FeatureTogglesMap$; isRgwRoute = document.location.href.includes(this.rgwUserUrlPrefix) || document.location.href.includes(this.rgwBuckerUrlPrefix) || document.location.href.includes(this.rgwRoleUrlPrefix) || - document.location.href.includes(this.rgwAccountsUrlPrefix); + document.location.href.includes(this.rgwAccountsUrlPrefix) || + document.location.href.includes(this.rgwMultisiteSyncPolicyPrefix); constructor( private authStorageService: AuthStorageService, @@ -57,7 +59,8 @@ export class ContextComponent implements OnInit, OnDestroy { this.rgwBuckerUrlPrefix, this.rgwUserUrlPrefix, this.rgwRoleUrlPrefix, - this.rgwAccountsUrlPrefix + this.rgwAccountsUrlPrefix, + this.rgwMultisiteSyncPolicyPrefix ].some((urlPrefix) => this.router.url.startsWith(urlPrefix))) ) ); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.spec.ts index 27b1e5fe545..0e41b6fd576 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.spec.ts @@ -1,6 +1,6 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; -import { configureTestBed } from '~/testing/unit-test-helper'; +import { configureTestBed, RgwHelper } from '~/testing/unit-test-helper'; import { RgwMultisiteService } from './rgw-multisite.service'; import { BlockUIModule } from 'ng-block-ui'; import { ToastrModule } from 'ngx-toastr'; @@ -39,6 +39,7 @@ describe('RgwMultisiteService', () => { beforeEach(() => { service = TestBed.inject(RgwMultisiteService); httpTesting = TestBed.inject(HttpTestingController); + RgwHelper.selectDaemon(); }); afterEach(() => { @@ -51,7 +52,9 @@ describe('RgwMultisiteService', () => { it('should fetch all the sync policy related or un-related to a bucket', () => { service.getSyncPolicy('', '', true).subscribe(); - const req = httpTesting.expectOne('api/rgw/multisite/sync-policy?all_policy=true'); + const req = httpTesting.expectOne( + `api/rgw/multisite/sync-policy?${RgwHelper.DAEMON_QUERY_PARAM}&all_policy=true` + ); expect(req.request.method).toBe('GET'); req.flush(mockSyncPolicyData); }); @@ -59,7 +62,9 @@ describe('RgwMultisiteService', () => { it('should create Sync Policy Group w/o bucket_name', () => { const postData = { group_id: 'test', status: 'enabled' }; service.createSyncPolicyGroup(postData).subscribe(); - const req = httpTesting.expectOne('api/rgw/multisite/sync-policy-group'); + const req = httpTesting.expectOne( + `api/rgw/multisite/sync-policy-group?${RgwHelper.DAEMON_QUERY_PARAM}` + ); expect(req.request.method).toBe('POST'); expect(req.request.body).toEqual(postData); req.flush(null); @@ -68,7 +73,9 @@ describe('RgwMultisiteService', () => { it('should create Sync Policy Group with bucket_name', () => { const postData = { group_id: 'test', status: 'enabled', bucket_name: 'test' }; service.createSyncPolicyGroup(postData).subscribe(); - const req = httpTesting.expectOne('api/rgw/multisite/sync-policy-group'); + const req = httpTesting.expectOne( + `api/rgw/multisite/sync-policy-group?${RgwHelper.DAEMON_QUERY_PARAM}` + ); expect(req.request.method).toBe('POST'); expect(req.request.body).toEqual(postData); req.flush(null); @@ -77,7 +84,9 @@ describe('RgwMultisiteService', () => { it('should modify Sync Policy Group', () => { const postData = { group_id: 'test', status: 'enabled', bucket_name: 'test' }; service.modifySyncPolicyGroup(postData).subscribe(); - const req = httpTesting.expectOne('api/rgw/multisite/sync-policy-group'); + const req = httpTesting.expectOne( + `api/rgw/multisite/sync-policy-group?${RgwHelper.DAEMON_QUERY_PARAM}` + ); expect(req.request.method).toBe('PUT'); expect(req.request.body).toEqual(postData); req.flush(null); @@ -86,14 +95,18 @@ describe('RgwMultisiteService', () => { it('should remove Sync Policy Group', () => { const group_id = 'test'; service.removeSyncPolicyGroup(group_id).subscribe(); - const req = httpTesting.expectOne('api/rgw/multisite/sync-policy-group/' + group_id); + const req = httpTesting.expectOne( + `api/rgw/multisite/sync-policy-group/${group_id}?${RgwHelper.DAEMON_QUERY_PARAM}` + ); expect(req.request.method).toBe('DELETE'); req.flush(null); }); it('should fetch the sync policy group with given group_id and bucket_name', () => { service.getSyncPolicyGroup('test', 'test').subscribe(); - const req = httpTesting.expectOne('api/rgw/multisite/sync-policy-group/test?bucket_name=test'); + const req = httpTesting.expectOne( + `api/rgw/multisite/sync-policy-group/test?${RgwHelper.DAEMON_QUERY_PARAM}&bucket_name=test` + ); expect(req.request.method).toBe('GET'); req.flush(mockSyncPolicyData[1]); }); @@ -107,7 +120,9 @@ describe('RgwMultisiteService', () => { zones: ['zone1-zg1-realm1'] }; service.createEditSyncFlow(payload).subscribe(); - const req = httpTesting.expectOne('api/rgw/multisite/sync-flow'); + const req = httpTesting.expectOne( + `api/rgw/multisite/sync-flow?${RgwHelper.DAEMON_QUERY_PARAM}` + ); expect(req.request.method).toBe('PUT'); expect(req.request.body).toEqual(payload); req.flush(null); @@ -123,7 +138,9 @@ describe('RgwMultisiteService', () => { destination_zone: ['zone1-zg2-realm2'] }; service.createEditSyncFlow(payload).subscribe(); - const req = httpTesting.expectOne('api/rgw/multisite/sync-flow'); + const req = httpTesting.expectOne( + `api/rgw/multisite/sync-flow?${RgwHelper.DAEMON_QUERY_PARAM}` + ); expect(req.request.method).toBe('PUT'); expect(req.request.body).toEqual(payload); req.flush(null); @@ -138,7 +155,9 @@ describe('RgwMultisiteService', () => { zones: ['zone1-zg1-realm1', 'zone2-zg1-realm1'] }; service.createEditSyncFlow(payload).subscribe(); - const req = httpTesting.expectOne('api/rgw/multisite/sync-flow'); + const req = httpTesting.expectOne( + `api/rgw/multisite/sync-flow?${RgwHelper.DAEMON_QUERY_PARAM}` + ); expect(req.request.method).toBe('PUT'); expect(req.request.body).toEqual(payload); req.flush(null); @@ -154,7 +173,9 @@ describe('RgwMultisiteService', () => { destination_zone: ['zone1-zg2-realm2', 'zone2-zg2-realm2'] }; service.createEditSyncFlow(payload).subscribe(); - const req = httpTesting.expectOne('api/rgw/multisite/sync-flow'); + const req = httpTesting.expectOne( + `api/rgw/multisite/sync-flow?${RgwHelper.DAEMON_QUERY_PARAM}` + ); expect(req.request.method).toBe('PUT'); expect(req.request.body).toEqual(payload); req.flush(null); @@ -163,7 +184,7 @@ describe('RgwMultisiteService', () => { it('should remove Symmetrical Sync flow', () => { service.removeSyncFlow('test', 'symmetrical', 'test', 'new-bucket').subscribe(); const req = httpTesting.expectOne( - `api/rgw/multisite/sync-flow/test/symmetrical/test?bucket_name=new-bucket` + `api/rgw/multisite/sync-flow/test/symmetrical/test?${RgwHelper.DAEMON_QUERY_PARAM}&bucket_name=new-bucket` ); expect(req.request.method).toBe('DELETE'); req.flush(null); @@ -172,7 +193,7 @@ describe('RgwMultisiteService', () => { it('should remove Directional Sync flow', () => { service.removeSyncFlow('test', 'directional', 'test', 'new-bucket').subscribe(); const req = httpTesting.expectOne( - `api/rgw/multisite/sync-flow/test/directional/test?bucket_name=new-bucket` + `api/rgw/multisite/sync-flow/test/directional/test?${RgwHelper.DAEMON_QUERY_PARAM}&bucket_name=new-bucket` ); expect(req.request.method).toBe('DELETE'); req.flush(null); @@ -187,7 +208,9 @@ describe('RgwMultisiteService', () => { group_id: 'sync-grp' }; service.createEditSyncPipe(payload).subscribe(); - const req = httpTesting.expectOne('api/rgw/multisite/sync-pipe'); + const req = httpTesting.expectOne( + `api/rgw/multisite/sync-pipe?${RgwHelper.DAEMON_QUERY_PARAM}` + ); expect(req.request.method).toBe('PUT'); expect(req.request.body).toEqual(payload); req.flush(null); @@ -202,7 +225,9 @@ describe('RgwMultisiteService', () => { group_id: 'sync-grp' }; service.createEditSyncFlow(payload).subscribe(); - const req = httpTesting.expectOne('api/rgw/multisite/sync-flow'); + const req = httpTesting.expectOne( + `api/rgw/multisite/sync-flow?${RgwHelper.DAEMON_QUERY_PARAM}` + ); expect(req.request.method).toBe('PUT'); expect(req.request.body).toEqual(payload); req.flush(null); @@ -211,7 +236,7 @@ describe('RgwMultisiteService', () => { it('should remove Sync Pipe', () => { service.removeSyncPipe('test', 'sync-grp', 'new-bucket').subscribe(); const req = httpTesting.expectOne( - `api/rgw/multisite/sync-pipe/sync-grp/test?bucket_name=new-bucket` + `api/rgw/multisite/sync-pipe/sync-grp/test?${RgwHelper.DAEMON_QUERY_PARAM}&bucket_name=new-bucket` ); expect(req.request.method).toBe('DELETE'); req.flush(null); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts index 7045c498455..b963c1139b8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts @@ -49,40 +49,47 @@ export class RgwMultisiteService { } getSyncPolicy(bucketName?: string, zonegroup?: string, fetchAllPolicy = false) { - let params = new HttpParams(); - if (bucketName) { - params = params.append('bucket_name', bucketName); - } - if (zonegroup) { - params = params.append('zonegroup_name', zonegroup); - } - // fetchAllPolicy - if true, will fetch all the policy either linked or not linked with the buckets - params = params.append('all_policy', fetchAllPolicy); - return this.http.get(`${this.url}/sync-policy`, { params }); + return this.rgwDaemonService.request((params: HttpParams) => { + if (bucketName) { + params = params.append('bucket_name', bucketName); + } + if (zonegroup) { + params = params.append('zonegroup_name', zonegroup); + } + // fetchAllPolicy - if true, will fetch all the policy either linked or not linked with the buckets + params = params.append('all_policy', fetchAllPolicy); + return this.http.get(`${this.url}/sync-policy`, { params }); + }); } getSyncPolicyGroup(group_id: string, bucket_name?: string) { - let params = new HttpParams(); - if (bucket_name) { - params = params.append('bucket_name', bucket_name); - } - return this.http.get(`${this.url}/sync-policy-group/${group_id}`, { params }); + return this.rgwDaemonService.request((params: HttpParams) => { + if (bucket_name) { + params = params.append('bucket_name', bucket_name); + } + return this.http.get(`${this.url}/sync-policy-group/${group_id}`, { params }); + }); } createSyncPolicyGroup(payload: { group_id: string; status: string; bucket_name?: string }) { - return this.http.post(`${this.url}/sync-policy-group`, payload); + return this.rgwDaemonService.request((params: HttpParams) => { + return this.http.post(`${this.url}/sync-policy-group`, payload, { params: params }); + }); } modifySyncPolicyGroup(payload: { group_id: string; status: string; bucket_name?: string }) { - return this.http.put(`${this.url}/sync-policy-group`, payload); + return this.rgwDaemonService.request((params: HttpParams) => { + return this.http.put(`${this.url}/sync-policy-group`, payload, { params: params }); + }); } removeSyncPolicyGroup(group_id: string, bucket_name?: string) { - let params = new HttpParams(); - if (bucket_name) { - params = params.append('bucket_name', bucket_name); - } - return this.http.delete(`${this.url}/sync-policy-group/${group_id}`, { params }); + return this.rgwDaemonService.request((params: HttpParams) => { + if (bucket_name) { + params = params.append('bucket_name', bucket_name); + } + return this.http.delete(`${this.url}/sync-policy-group/${group_id}`, { params }); + }); } setUpMultisiteReplication( @@ -130,42 +137,47 @@ export class RgwMultisiteService { } createEditSyncFlow(payload: any) { - return this.http.put(`${this.url}/sync-flow`, payload); + return this.rgwDaemonService.request((params: HttpParams) => { + return this.http.put(`${this.url}/sync-flow`, payload, { params: params }); + }); } removeSyncFlow(flow_id: string, flow_type: string, group_id: string, bucket_name?: string) { - let params = new HttpParams(); - if (bucket_name) { - params = params.append('bucket_name', encodeURIComponent(bucket_name)); - } - return this.http.delete( - `${this.url}/sync-flow/${encodeURIComponent(flow_id)}/${flow_type}/${encodeURIComponent( - group_id - )}`, - { params } - ); + return this.rgwDaemonService.request((params: HttpParams) => { + if (bucket_name) { + params = params.append('bucket_name', encodeURIComponent(bucket_name)); + } + return this.http.delete( + `${this.url}/sync-flow/${encodeURIComponent(flow_id)}/${flow_type}/${encodeURIComponent( + group_id + )}`, + { params } + ); + }); } createEditSyncPipe(payload: any, user?: string, mode?: string) { - let params = new HttpParams(); - if (user) { - params = params.append('user', user); - } - if (mode) { - params = params.append('mode', mode); - } - return this.http.put(`${this.url}/sync-pipe`, payload, { params }); + return this.rgwDaemonService.request((params: HttpParams) => { + if (user) { + params = params.append('user', user); + } + if (mode) { + params = params.append('mode', mode); + } + return this.http.put(`${this.url}/sync-pipe`, payload, { params }); + }); } removeSyncPipe(pipe_id: string, group_id: string, bucket_name?: string) { - let params = new HttpParams(); - if (bucket_name) { - params = params.append('bucket_name', encodeURIComponent(bucket_name)); - } - return this.http.delete( - `${this.url}/sync-pipe/${encodeURIComponent(group_id)}/${encodeURIComponent(pipe_id)}`, - { params } - ); + return this.rgwDaemonService.request((params: HttpParams) => { + if (bucket_name) { + params = params.append('bucket_name', encodeURIComponent(bucket_name)); + } + return this.http.delete( + `${this.url}/sync-pipe/${encodeURIComponent(group_id)}/${encodeURIComponent(pipe_id)}`, + { params } + ); + }); } setRestartGatewayMessage(value: boolean): void { diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index a43c312b4e6..d2e5aca503a 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -18914,6 +18914,9 @@ paths: bucket_name: default: '' type: string + daemon_name: + default: '' + type: string destination_zone: type: string flow_id: @@ -19002,6 +19005,11 @@ paths: name: bucket_name schema: type: string + - default: '' + in: query + name: daemon_name + schema: + type: string responses: '202': content: @@ -19046,6 +19054,9 @@ paths: bucket_name: default: '' type: string + daemon_name: + default: '' + type: string destination_bucket: default: '' type: string @@ -19133,6 +19144,11 @@ paths: name: bucket_name schema: type: string + - default: '' + in: query + name: daemon_name + schema: + type: string responses: '202': content: @@ -19184,6 +19200,11 @@ paths: name: all_policy schema: type: string + - default: '' + in: query + name: daemon_name + schema: + type: string responses: '200': content: @@ -19219,6 +19240,9 @@ paths: bucket_name: default: '' type: string + daemon_name: + default: '' + type: string group_id: type: string status: @@ -19270,6 +19294,9 @@ paths: bucket_name: default: '' type: string + daemon_name: + default: '' + type: string group_id: type: string status: @@ -19324,6 +19351,11 @@ paths: name: bucket_name schema: type: string + - default: '' + in: query + name: daemon_name + schema: + type: string responses: '202': content: @@ -19369,6 +19401,11 @@ paths: name: bucket_name schema: type: string + - default: '' + in: query + name: daemon_name + schema: + type: string responses: '200': content: diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py index d9fbc71e615..c005ae8e828 100755 --- a/src/pybind/mgr/dashboard/services/rgw_client.py +++ b/src/pybind/mgr/dashboard/services/rgw_client.py @@ -1396,9 +1396,9 @@ class RgwMultisiteAutomation: try: rgw_multisite_instance = RgwMultisite() self.update_progress( - f"Initializing multi-site configuration || Creating realm: {realm}, \ - zonegroup: {zg}, and zone: {zone} along \ - with system user: {username}" + f"Initializing multi-site configuration || Creating realm: " + f"{realm}, zonegroup: {zg}, and zone: {zone} along " + f"with system user: {username}" ) rgw_multisite_instance.create_realm(realm_name=realm, default=True) rgw_multisite_instance.create_zonegroup(realm_name=realm, zonegroup_name=zg, @@ -1495,9 +1495,9 @@ class RgwMultisiteAutomation: self.progress_done += 1 self.update_progress( - f"Verifying system user and completing replication setup on \ - cluster {cluster_fsid} || Ensuring presence of user '{username}' \ - and assigning necessary RGW credentials" + f"Verifying system user and completing replication setup on " + f"cluster {cluster_fsid} || Ensuring presence of user " + f"'{username}' and assigning necessary RGW credentials" ) self._verify_user_and_daemons(cluster_url, cluster_token, realm_name, @@ -2690,6 +2690,50 @@ class RgwMultisite: raise DashboardException(error, http_status_code=500, component='rgw') return user_list + def get_realm_from_daemon(self, daemon_name: Optional[str] = None) -> Optional[str]: + """Extract realm_name from daemon if daemon_name is provided.""" + if not daemon_name: + return None + try: + daemons = _get_daemons() + return daemons[daemon_name].realm_name + except (KeyError, AttributeError): + # If daemon not found or has no realm_name, return None + return None + + def get_zonegroup_from_daemon(self, daemon_name: Optional[str] = None) -> Optional[str]: + """Extract zonegroup_name from daemon if daemon_name is provided.""" + if not daemon_name: + return None + try: + daemons = _get_daemons() + return daemons[daemon_name].zonegroup_name + except (KeyError, AttributeError): + # If daemon not found or has no zonegroup_name, return None + return None + + def _add_realm_zonegroup_args( + self, cmd: List[str], daemon_name: Optional[str] = None, + explicit_zonegroup: str = '') -> None: + """ + Add --rgw-realm and --rgw-zonegroup arguments to a radosgw-admin command. + + Args: + cmd: The command list to append arguments to + daemon_name: The daemon name to extract realm/zonegroup from + explicit_zonegroup: If provided, use this instead of daemon's zonegroup + """ + realm_name = self.get_realm_from_daemon(daemon_name) + if realm_name: + cmd += ['--rgw-realm', realm_name] + + if explicit_zonegroup: + zonegroup_name: Optional[str] = explicit_zonegroup + else: + zonegroup_name = self.get_zonegroup_from_daemon(daemon_name) + if zonegroup_name: + cmd += ['--rgw-zonegroup', zonegroup_name] + def get_multisite_status(self): is_multisite_configured = True rgw_realm_list = self.list_realms() @@ -2805,12 +2849,20 @@ class RgwMultisite: return '' - def get_sync_policy(self, bucket_name: str = '', zonegroup_name: str = ''): + def get_sync_policy(self, bucket_name: str = '', + zonegroup_name: str = '', + daemon_name: Optional[str] = None): rgw_sync_policy_cmd = ['sync', 'policy', 'get'] if bucket_name: rgw_sync_policy_cmd += ['--bucket', bucket_name] - if zonegroup_name: - rgw_sync_policy_cmd += ['--rgw-zonegroup', zonegroup_name] + # Use daemon's realm and zonegroup when daemon_name is provided + realm_name = self.get_realm_from_daemon(daemon_name) + if realm_name: + rgw_sync_policy_cmd += ['--rgw-realm', realm_name] + # Using daemon's zonegroup if no explicit zonegroup provided + target_zonegroup = zonegroup_name or self.get_zonegroup_from_daemon(daemon_name) + if target_zonegroup: + rgw_sync_policy_cmd += ['--rgw-zonegroup', target_zonegroup] try: exit_code, out, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd) if exit_code > 0: @@ -2821,12 +2873,11 @@ class RgwMultisite: raise DashboardException(error, http_status_code=500, component='rgw') def get_sync_policy_group(self, group_id: str, bucket_name: str = '', - zonegroup_name: str = ''): + zonegroup_name: str = '', daemon_name: Optional[str] = None): 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] + self._add_realm_zonegroup_args(rgw_sync_policy_cmd, daemon_name, zonegroup_name) try: exit_code, out, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd) if exit_code > 0: @@ -2837,11 +2888,12 @@ class RgwMultisite: raise DashboardException(error, http_status_code=500, component='rgw') def create_sync_policy_group(self, group_id: str, status: str, bucket_name: str = '', - update_period=False): + update_period=False, daemon_name: Optional[str] = None): rgw_sync_policy_cmd = ['sync', 'group', 'create', '--group-id', group_id, '--status', SyncStatus[status].value] if bucket_name: rgw_sync_policy_cmd += ['--bucket', bucket_name] + self._add_realm_zonegroup_args(rgw_sync_policy_cmd, daemon_name) try: exit_code, _, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd) if exit_code > 0: @@ -2850,14 +2902,16 @@ class RgwMultisite: except SubprocessError as error: raise DashboardException(error, http_status_code=500, component='rgw') if not bucket_name and update_period: - self.update_period() + realm_name = self.get_realm_from_daemon(daemon_name) + self.update_period(realm_name=realm_name) def update_sync_policy_group(self, group_id: str, status: str, bucket_name: str = '', - update_period=False): + update_period=False, daemon_name: Optional[str] = None): rgw_sync_policy_cmd = ['sync', 'group', 'modify', '--group-id', group_id, '--status', SyncStatus[status].value] if bucket_name: rgw_sync_policy_cmd += ['--bucket', bucket_name] + self._add_realm_zonegroup_args(rgw_sync_policy_cmd, daemon_name) try: exit_code, _, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd) if exit_code > 0: @@ -2866,12 +2920,16 @@ class RgwMultisite: except SubprocessError as error: raise DashboardException(error, http_status_code=500, component='rgw') if not bucket_name and update_period: - self.update_period() + realm_name = self.get_realm_from_daemon(daemon_name) + self.update_period(realm_name=realm_name) - def remove_sync_policy_group(self, group_id: str, bucket_name='', update_period=False): + def remove_sync_policy_group(self, group_id: str, bucket_name='', + update_period=False, + daemon_name: Optional[str] = None): rgw_sync_policy_cmd = ['sync', 'group', 'remove', '--group-id', group_id] if bucket_name: rgw_sync_policy_cmd += ['--bucket', bucket_name] + self._add_realm_zonegroup_args(rgw_sync_policy_cmd, daemon_name) try: exit_code, _, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd) if exit_code > 0: @@ -2880,18 +2938,20 @@ class RgwMultisite: except SubprocessError as error: raise DashboardException(error, http_status_code=500, component='rgw') if not bucket_name and update_period: - self.update_period() + realm_name = self.get_realm_from_daemon(daemon_name) + self.update_period(realm_name=realm_name) def create_sync_flow(self, group_id: str, flow_id: str, flow_type: str, zones: Optional[Dict[str, List]] = None, bucket_name: str = '', source_zone: Optional[str] = None, destination_zone: Optional[str] = None, - update_period=False): + update_period=False, daemon_name: Optional[str] = None): rgw_sync_policy_cmd = ['sync', 'group', 'flow', 'create', '--group-id', group_id, '--flow-id', flow_id, '--flow-type', SyncFlowTypes[flow_type].value] if bucket_name: rgw_sync_policy_cmd += ['--bucket', bucket_name] + self._add_realm_zonegroup_args(rgw_sync_policy_cmd, daemon_name) if SyncFlowTypes[flow_type].value == 'directional': @@ -2925,15 +2985,18 @@ class RgwMultisite: raise DashboardException(error, http_status_code=500, component='rgw') if len(zones['removed']) > 0: - self.remove_sync_flow(group_id, flow_id, flow_type, source_zone, - destination_zone, zones['removed'], bucket_name) + self.remove_sync_flow( + group_id, flow_id, flow_type, source_zone, + destination_zone, zones['removed'], bucket_name, + daemon_name=daemon_name) if not bucket_name and update_period: - self.update_period() + realm_name = self.get_realm_from_daemon(daemon_name) + self.update_period(realm_name=realm_name) def remove_sync_flow(self, group_id: str, flow_id: str, flow_type: str, source_zone='', destination_zone='', zones: Optional[List[str]] = None, bucket_name: str = '', - update_period=False): + update_period=False, daemon_name: Optional[str] = None): rgw_sync_policy_cmd = ['sync', 'group', 'flow', 'remove', '--group-id', group_id, '--flow-id', flow_id, '--flow-type', SyncFlowTypes[flow_type].value] @@ -2945,6 +3008,7 @@ class RgwMultisite: if bucket_name: rgw_sync_policy_cmd += ['--bucket', bucket_name] + self._add_realm_zonegroup_args(rgw_sync_policy_cmd, daemon_name) logger.info("Removing sync flow! %s", rgw_sync_policy_cmd) try: @@ -2955,7 +3019,8 @@ class RgwMultisite: except SubprocessError as error: raise DashboardException(error, http_status_code=500, component='rgw') if not bucket_name and update_period: - self.update_period() + realm_name = self.get_realm_from_daemon(daemon_name) + self.update_period(realm_name=realm_name) def create_sync_pipe(self, group_id: str, pipe_id: str, source_zones: Dict[str, Any], @@ -2964,7 +3029,7 @@ class RgwMultisite: destination_bucket: str = '', bucket_name: str = '', update_period=False, - user: str = '', mode: str = ''): + user: str = '', mode: str = '', daemon_name: Optional[str] = None): if source_zones['added'] or destination_zones['added']: rgw_sync_policy_cmd = ['sync', 'group', 'pipe', 'create', @@ -2972,6 +3037,7 @@ class RgwMultisite: if bucket_name: rgw_sync_policy_cmd += ['--bucket', bucket_name] + self._add_realm_zonegroup_args(rgw_sync_policy_cmd, daemon_name) rgw_sync_policy_cmd += ['--source-bucket', source_bucket] @@ -2998,24 +3064,26 @@ class RgwMultisite: except SubprocessError as error: raise DashboardException(error, http_status_code=500, component='rgw') if not bucket_name and update_period: - self.update_period() + realm_name = self.get_realm_from_daemon(daemon_name) + self.update_period(realm_name=realm_name) if ((source_zones['removed'] and '*' not in source_zones['added']) or (destination_zones['removed'] and '*' not in destination_zones['added'])): self.remove_sync_pipe(group_id, pipe_id, source_zones['removed'], destination_zones['removed'], - bucket_name, True) + bucket_name, True, daemon_name=daemon_name) def remove_sync_pipe(self, group_id: str, pipe_id: str, source_zones: Optional[List[str]] = None, destination_zones: Optional[List[str]] = None, bucket_name: str = '', - update_period=False): + update_period=False, daemon_name: Optional[str] = None): rgw_sync_policy_cmd = ['sync', 'group', 'pipe', 'remove', '--group-id', group_id, '--pipe-id', pipe_id] if bucket_name: rgw_sync_policy_cmd += ['--bucket', bucket_name] + self._add_realm_zonegroup_args(rgw_sync_policy_cmd, daemon_name) if source_zones: rgw_sync_policy_cmd += ['--source-zones', ','.join(source_zones)] @@ -3032,7 +3100,8 @@ class RgwMultisite: except SubprocessError as error: raise DashboardException(error, http_status_code=500, component='rgw') if not bucket_name and update_period: - self.update_period() + realm_name = self.get_realm_from_daemon(daemon_name) + self.update_period(realm_name=realm_name) def create_dashboard_admin_sync_group(self, zonegroup_name: str = ''): @@ -3095,9 +3164,9 @@ class RgwTopicmanagement: def push_endpoint_password(push_endpoint: str) -> str: parsed = urlparse(push_endpoint) if parsed.username and parsed.password: - netloc = f"{parsed.username}:****@{parsed.hostname}" + netloc = f"{parsed.username}: ****@{parsed.hostname}" if parsed.port: - netloc += f":{parsed.port}" + netloc += f": {parsed.port}" parsed = parsed._replace(netloc=netloc) return urlunparse(parsed) return push_endpoint diff --git a/src/pybind/mgr/dashboard/tests/test_rgw_client.py b/src/pybind/mgr/dashboard/tests/test_rgw_client.py index 898803dd29a..b5b350fd803 100644 --- a/src/pybind/mgr/dashboard/tests/test_rgw_client.py +++ b/src/pybind/mgr/dashboard/tests/test_rgw_client.py @@ -7,7 +7,7 @@ from unittest.mock import Mock, patch from .. import mgr from ..exceptions import DashboardException from ..services.rgw_client import NoRgwDaemonsException, RgwClient, \ - _determine_rgw_addr, _parse_frontend_config + RgwMultisite, _determine_rgw_addr, _parse_frontend_config from ..services.service import NoCredentialsException from ..settings import Settings from ..tests import CLICommandTestMixin, RgwStub @@ -451,3 +451,259 @@ class TestDictToXML(TestCase): expected_xml = "Foo\n30\n" result = RgwClient.dict_to_xml(data) self.assertEqual(result, expected_xml) + + +class RgwMultisiteTest(TestCase): + """Test cases for RgwMultisite class with daemon_name parameter support.""" + + def setUp(self): + RgwStub.get_daemons() + + @patch('dashboard.services.rgw_client.mgr.send_rgwadmin_command') + @patch('dashboard.services.rgw_client._get_daemons') + def test_get_sync_policy_with_daemon_name(self, mock_get_daemons, mock_send_command): + """Test get_sync_policy with daemon_name parameter.""" + mock_daemon = Mock() + mock_daemon.realm_name = 'test_realm' + mock_daemon.zonegroup_name = 'test_zonegroup' + mock_get_daemons.return_value = {'test_daemon': mock_daemon} + + mock_send_command.return_value = (0, {'groups': []}, '') + + multisite = RgwMultisite() + result = multisite.get_sync_policy(bucket_name='test_bucket', daemon_name='test_daemon') + + self.assertEqual(result, {'groups': []}) + # Verify the command includes realm and zonegroup + call_args = mock_send_command.call_args[0][0] + self.assertIn('--rgw-realm', call_args) + self.assertIn('test_realm', call_args) + self.assertIn('--rgw-zonegroup', call_args) + self.assertIn('test_zonegroup', call_args) + + @patch('dashboard.services.rgw_client.mgr.send_rgwadmin_command') + @patch('dashboard.services.rgw_client._get_daemons') + def test_get_sync_policy_group_with_daemon_name(self, mock_get_daemons, mock_send_command): + """Test get_sync_policy_group with daemon_name parameter.""" + mock_daemon = Mock() + mock_daemon.realm_name = 'test_realm' + mock_daemon.zonegroup_name = 'test_zonegroup' + mock_get_daemons.return_value = {'test_daemon': mock_daemon} + + mock_send_command.return_value = (0, {'id': 'test_group'}, '') + + multisite = RgwMultisite() + result = multisite.get_sync_policy_group('test_group', daemon_name='test_daemon') + + self.assertEqual(result, {'id': 'test_group'}) + call_args = mock_send_command.call_args[0][0] + self.assertIn('--rgw-realm', call_args) + self.assertIn('test_realm', call_args) + + @patch('dashboard.services.rgw_client.mgr.send_rgwadmin_command') + @patch('dashboard.services.rgw_client._get_daemons') + def test_create_sync_policy_group_with_daemon_name(self, mock_get_daemons, mock_send_command): + """Test create_sync_policy_group with daemon_name parameter.""" + mock_daemon = Mock() + mock_daemon.realm_name = 'test_realm' + mock_daemon.zonegroup_name = 'test_zonegroup' + mock_get_daemons.return_value = {'test_daemon': mock_daemon} + + mock_send_command.return_value = (0, '', '') + + multisite = RgwMultisite() + multisite.create_sync_policy_group('test_group', 'enabled', daemon_name='test_daemon') + + call_args = mock_send_command.call_args[0][0] + self.assertIn('sync', call_args) + self.assertIn('group', call_args) + self.assertIn('create', call_args) + self.assertIn('--rgw-realm', call_args) + self.assertIn('test_realm', call_args) + + @patch('dashboard.services.rgw_client.mgr.send_rgwadmin_command') + @patch('dashboard.services.rgw_client._get_daemons') + def test_update_sync_policy_group_with_daemon_name(self, mock_get_daemons, mock_send_command): + """Test update_sync_policy_group with daemon_name parameter.""" + mock_daemon = Mock() + mock_daemon.realm_name = 'test_realm' + mock_daemon.zonegroup_name = 'test_zonegroup' + mock_get_daemons.return_value = {'test_daemon': mock_daemon} + + mock_send_command.return_value = (0, '', '') + + multisite = RgwMultisite() + multisite.update_sync_policy_group('test_group', 'enabled', daemon_name='test_daemon') + + call_args = mock_send_command.call_args[0][0] + self.assertIn('sync', call_args) + self.assertIn('group', call_args) + self.assertIn('modify', call_args) + self.assertIn('--rgw-realm', call_args) + + @patch('dashboard.services.rgw_client.mgr.send_rgwadmin_command') + @patch('dashboard.services.rgw_client._get_daemons') + def test_remove_sync_policy_group_with_daemon_name(self, mock_get_daemons, mock_send_command): + """Test remove_sync_policy_group with daemon_name parameter.""" + mock_daemon = Mock() + mock_daemon.realm_name = 'test_realm' + mock_daemon.zonegroup_name = 'test_zonegroup' + mock_get_daemons.return_value = {'test_daemon': mock_daemon} + + mock_send_command.return_value = (0, '', '') + + multisite = RgwMultisite() + multisite.remove_sync_policy_group('test_group', daemon_name='test_daemon') + + call_args = mock_send_command.call_args[0][0] + self.assertIn('sync', call_args) + self.assertIn('group', call_args) + self.assertIn('remove', call_args) + self.assertIn('--rgw-realm', call_args) + + @patch('dashboard.services.rgw_client.mgr.send_rgwadmin_command') + @patch('dashboard.services.rgw_client._get_daemons') + def test_create_sync_flow_with_daemon_name(self, mock_get_daemons, mock_send_command): + """Test create_sync_flow with daemon_name parameter.""" + mock_daemon = Mock() + mock_daemon.realm_name = 'test_realm' + mock_daemon.zonegroup_name = 'test_zonegroup' + mock_get_daemons.return_value = {'test_daemon': mock_daemon} + + mock_send_command.return_value = (0, '', '') + + multisite = RgwMultisite() + # For symmetrical flow, need to provide zones with added zones + multisite.create_sync_flow( + 'test_group', 'test_flow', 'symmetrical', + zones={'added': ['zone1', 'zone2'], 'removed': []}, + daemon_name='test_daemon' + ) + + call_args = mock_send_command.call_args[0][0] + self.assertIn('sync', call_args) + self.assertIn('group', call_args) + self.assertIn('flow', call_args) + self.assertIn('create', call_args) + self.assertIn('--rgw-realm', call_args) + + @patch('dashboard.services.rgw_client.mgr.send_rgwadmin_command') + @patch('dashboard.services.rgw_client._get_daemons') + def test_remove_sync_flow_with_daemon_name(self, mock_get_daemons, mock_send_command): + """Test remove_sync_flow with daemon_name parameter.""" + mock_daemon = Mock() + mock_daemon.realm_name = 'test_realm' + mock_daemon.zonegroup_name = 'test_zonegroup' + mock_get_daemons.return_value = {'test_daemon': mock_daemon} + + mock_send_command.return_value = (0, '', '') + + multisite = RgwMultisite() + multisite.remove_sync_flow( + 'test_group', 'test_flow', 'symmetrical', + daemon_name='test_daemon' + ) + + call_args = mock_send_command.call_args[0][0] + self.assertIn('sync', call_args) + self.assertIn('group', call_args) + self.assertIn('flow', call_args) + self.assertIn('remove', call_args) + self.assertIn('--rgw-realm', call_args) + + @patch('dashboard.services.rgw_client.mgr.send_rgwadmin_command') + @patch('dashboard.services.rgw_client._get_daemons') + def test_create_sync_pipe_with_daemon_name(self, mock_get_daemons, mock_send_command): + """Test create_sync_pipe with daemon_name parameter.""" + mock_daemon = Mock() + mock_daemon.realm_name = 'test_realm' + mock_daemon.zonegroup_name = 'test_zonegroup' + mock_get_daemons.return_value = {'test_daemon': mock_daemon} + + mock_send_command.return_value = (0, '', '') + + multisite = RgwMultisite() + multisite.create_sync_pipe( + 'test_group', 'test_pipe', + {'added': ['zone1'], 'removed': []}, + {'added': ['zone2'], 'removed': []}, + 'source_bucket', + daemon_name='test_daemon' + ) + + call_args = mock_send_command.call_args[0][0] + self.assertIn('sync', call_args) + self.assertIn('group', call_args) + self.assertIn('pipe', call_args) + self.assertIn('create', call_args) + self.assertIn('--rgw-realm', call_args) + + @patch('dashboard.services.rgw_client.mgr.send_rgwadmin_command') + @patch('dashboard.services.rgw_client._get_daemons') + def test_remove_sync_pipe_with_daemon_name(self, mock_get_daemons, mock_send_command): + """Test remove_sync_pipe with daemon_name parameter.""" + mock_daemon = Mock() + mock_daemon.realm_name = 'test_realm' + mock_daemon.zonegroup_name = 'test_zonegroup' + mock_get_daemons.return_value = {'test_daemon': mock_daemon} + + mock_send_command.return_value = (0, '', '') + + multisite = RgwMultisite() + multisite.remove_sync_pipe( + 'test_group', 'test_pipe', + source_zones=['zone1'], + destination_zones=['zone2'], + daemon_name='test_daemon' + ) + + call_args = mock_send_command.call_args[0][0] + self.assertIn('sync', call_args) + self.assertIn('group', call_args) + self.assertIn('pipe', call_args) + self.assertIn('remove', call_args) + self.assertIn('--rgw-realm', call_args) + + @patch('dashboard.services.rgw_client._get_daemons') + def test_get_zonegroup_from_daemon(self, mock_get_daemons): + """Test get_zonegroup_from_daemon method.""" + mock_daemon = Mock() + mock_daemon.zonegroup_name = 'test_zonegroup' + mock_get_daemons.return_value = {'test_daemon': mock_daemon} + + multisite = RgwMultisite() + result = multisite.get_zonegroup_from_daemon('test_daemon') + + self.assertEqual(result, 'test_zonegroup') + + @patch('dashboard.services.rgw_client._get_daemons') + def test_get_zonegroup_from_daemon_not_found(self, mock_get_daemons): + """Test get_zonegroup_from_daemon with non-existent daemon.""" + mock_get_daemons.return_value = {} + + multisite = RgwMultisite() + result = multisite.get_zonegroup_from_daemon('non_existent_daemon') + + self.assertIsNone(result) + + @patch('dashboard.services.rgw_client._get_daemons') + def test_get_realm_from_daemon(self, mock_get_daemons): + """Test get_realm_from_daemon method.""" + mock_daemon = Mock() + mock_daemon.realm_name = 'test_realm' + mock_get_daemons.return_value = {'test_daemon': mock_daemon} + + multisite = RgwMultisite() + result = multisite.get_realm_from_daemon('test_daemon') + + self.assertEqual(result, 'test_realm') + + @patch('dashboard.services.rgw_client._get_daemons') + def test_get_realm_from_daemon_not_found(self, mock_get_daemons): + """Test get_realm_from_daemon with non-existent daemon.""" + mock_get_daemons.return_value = {} + + multisite = RgwMultisite() + result = multisite.get_realm_from_daemon('non_existent_daemon') + + self.assertIsNone(result)