From: Naman Munet Date: Tue, 25 Jun 2024 11:54:51 +0000 (+0530) Subject: mgr/dashborad: RGW sync policy X-Git-Tag: v19.1.1~94^2~3 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=41014fce446790481ee4f38de68b2ab8d467fbfe;p=ceph.git mgr/dashborad: RGW sync policy Fixes: https://tracker.ceph.com/issues/66576 Signed-off-by: Naman Munet (cherry picked from commit 94f9a2bf18f43cc5b05dd8c216fe9e0975969106) --- diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index 08f62a2f315f7..9c5d1106eb467 100644 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -129,8 +129,21 @@ class RgwMultisiteController(RESTController): @Endpoint(path='/sync-policy') @EndpointDoc("Get the sync policy") @ReadPermission - def get_sync_policy(self, bucket_name='', zonegroup_name=''): + def get_sync_policy(self, bucket_name='', zonegroup_name='', all_policy=None): multisite_instance = RgwMultisite() + all_policy = str_to_bool(all_policy) + if all_policy: + sync_policy_list = [] + buckets = json.loads(RgwBucket().list(stats=False)) + 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) + for policy in other_sync_policy['groups']: + sync_policy_list.append(policy) + return sync_policy_list return multisite_instance.get_sync_policy(bucket_name, zonegroup_name) @Endpoint(path='/sync-policy-group') diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/multisite.e2e.spec.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/multisite.e2e.spec.ts new file mode 100644 index 0000000000000..13f893035e9b8 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/multisite.e2e.spec.ts @@ -0,0 +1,30 @@ +import { MultisitePageHelper } from './multisite.po'; + +describe('Multisite page', () => { + const multisite = new MultisitePageHelper(); + + beforeEach(() => { + cy.login(); + multisite.navigateTo(); + }); + + describe('tabs and table tests', () => { + it('should show two tabs', () => { + multisite.getTabsCount().should('eq', 2); + }); + + it('should show Configuration tab as a first tab', () => { + multisite.getTabText(0).should('eq', 'Configuration'); + }); + + it('should show sync policy tab as a second tab', () => { + multisite.getTabText(1).should('eq', 'Sync Policy'); + }); + + it('should show empty table in Sync Policy page', () => { + multisite.getTab('Sync Policy').click(); + multisite.getDataTables().should('exist'); + multisite.getTableCount('total').should('eq', 0); + }); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/multisite.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/multisite.po.ts new file mode 100644 index 0000000000000..b48b58e8ee38e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/multisite.po.ts @@ -0,0 +1,8 @@ +import { PageHelper } from '../page-helper.po'; + +const pages = { + index: { url: '#/rgw/multisite', id: 'cd-rgw-multisite-details' } +}; +export class MultisitePageHelper extends PageHelper { + pages = pages; +} diff --git a/src/pybind/mgr/dashboard/frontend/jest.config.cjs b/src/pybind/mgr/dashboard/frontend/jest.config.cjs index 9cdf6be4b4637..0dbe5e159471c 100644 --- a/src/pybind/mgr/dashboard/frontend/jest.config.cjs +++ b/src/pybind/mgr/dashboard/frontend/jest.config.cjs @@ -32,7 +32,7 @@ const jestConfig = { }, setupFiles: ['jest-canvas-mock'], coverageReporters: ['cobertura', 'html'], - modulePathIgnorePatterns: ['/coverage/', '/node_modules/simplebar-angular'], + modulePathIgnorePatterns: ['/coverage/', '/node_modules/simplebar-angular', '/cypress'], testMatch: ['**/*.spec.ts'], testRunner: 'jest-jasmine2' }; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html index 291013a5ce2f1..f6b9bbae8f33e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html @@ -1,123 +1,155 @@ -
-
-
- In order to access the import/export feature, the rgw module must be enabled + + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts index be65424cf7ab9..ef833a0324ce8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.spec.ts @@ -8,6 +8,7 @@ import { SharedModule } from '~/app/shared/shared.module'; import { RgwMultisiteDetailsComponent } from './rgw-multisite-details.component'; import { RouterTestingModule } from '@angular/router/testing'; import { configureTestBed } from '~/testing/unit-test-helper'; +import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; describe('RgwMultisiteDetailsComponent', () => { let component: RgwMultisiteDetailsComponent; @@ -21,7 +22,8 @@ describe('RgwMultisiteDetailsComponent', () => { TreeModule, SharedModule, ToastrModule.forRoot(), - RouterTestingModule + RouterTestingModule, + NgbNavModule ] }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts index 6e898e789456b..d5e0fd063f8d5 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts @@ -267,7 +267,6 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { } }); } - /* setConfigValues() { this.rgwDaemonService .setMultisiteConfig( @@ -589,4 +588,19 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { } ); } + + onNavChange(event: any) { + if (event.nextId == 'configuration') { + this.metadata = null; + /* + It is a known issue with angular2-tree package when tree is hidden (for example inside tab or modal), + it is not rendered when it becomes visible. Solution is to call this.tree.sizeChanged() which recalculates + the rendered nodes according to the actual viewport size. (https://angular2-tree.readme.io/docs/common-issues) + */ + setTimeout(() => { + this.tree.sizeChanged(); + this.onUpdateData(); + }, 200); + } + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy/rgw-multisite-sync-policy.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy/rgw-multisite-sync-policy.component.html new file mode 100644 index 0000000000000..7de60b56e6834 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy/rgw-multisite-sync-policy.component.html @@ -0,0 +1,18 @@ + + Multisite Sync Policy + + Multisite bucket-granularity sync policy provides fine grained control of data movement between buckets in different zones. + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy/rgw-multisite-sync-policy.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy/rgw-multisite-sync-policy.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy/rgw-multisite-sync-policy.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy/rgw-multisite-sync-policy.component.spec.ts new file mode 100644 index 0000000000000..6811f867e23be --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy/rgw-multisite-sync-policy.component.spec.ts @@ -0,0 +1,26 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RgwMultisiteSyncPolicyComponent } from './rgw-multisite-sync-policy.component'; +import { HttpClientModule } from '@angular/common/http'; +import { TitleCasePipe } from '@angular/common'; + +describe('RgwMultisiteSyncPolicyComponent', () => { + let component: RgwMultisiteSyncPolicyComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RgwMultisiteSyncPolicyComponent], + imports: [HttpClientModule], + providers: [TitleCasePipe] + }).compileComponents(); + + fixture = TestBed.createComponent(RgwMultisiteSyncPolicyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy/rgw-multisite-sync-policy.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy/rgw-multisite-sync-policy.component.ts new file mode 100644 index 0000000000000..77b49a9ba1806 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy/rgw-multisite-sync-policy.component.ts @@ -0,0 +1,74 @@ +import { TitleCasePipe } from '@angular/common'; +import { Component, OnInit } from '@angular/core'; +import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service'; +import { CellTemplate } from '~/app/shared/enum/cell-template.enum'; +import { CdTableColumn } from '~/app/shared/models/cd-table-column'; + +@Component({ + selector: 'cd-rgw-multisite-sync-policy', + templateUrl: './rgw-multisite-sync-policy.component.html', + styleUrls: ['./rgw-multisite-sync-policy.component.scss'] +}) +export class RgwMultisiteSyncPolicyComponent implements OnInit { + columns: Array = []; + syncPolicyData: any = []; + + constructor( + private rgwMultisiteService: RgwMultisiteService, + private titleCasePipe: TitleCasePipe + ) {} + + ngOnInit(): void { + this.columns = [ + { + name: $localize`Group Name`, + prop: 'groupName', + flexGrow: 1 + }, + { + name: $localize`Status`, + prop: 'status', + flexGrow: 1, + cellTransformation: CellTemplate.tooltip, + customTemplateConfig: { + map: { + Enabled: { class: 'badge-success', tooltip: 'sync is allowed and enabled' }, + Allowed: { class: 'badge-info', tooltip: 'sync is allowed' }, + Forbidden: { + class: 'badge-warning', + tooltip: + 'sync (as defined by this group) is not allowed and can override other groups' + } + } + }, + pipe: this.titleCasePipe + }, + { + name: $localize`Zonegroup`, + prop: 'zonegroup', + flexGrow: 1 + }, + { + name: $localize`Bucket`, + prop: 'bucket', + flexGrow: 1 + } + ]; + + this.rgwMultisiteService + .getSyncPolicy('', '', true) + .subscribe((allSyncPolicyData: Array) => { + if (allSyncPolicyData && allSyncPolicyData.length > 0) { + allSyncPolicyData.forEach((policy) => { + this.syncPolicyData.push({ + groupName: policy['id'], + status: policy['status'], + bucket: policy['bucketName'], + zonegroup: '' + }); + }); + this.syncPolicyData = [...this.syncPolicyData]; + } + }); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts index d893bd688273c..d07d8f1bf4901 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts @@ -1,4 +1,4 @@ -import { CommonModule } from '@angular/common'; +import { CommonModule, TitleCasePipe } from '@angular/common'; import { NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule, Routes } from '@angular/router'; @@ -49,6 +49,7 @@ import { RgwSyncDataInfoComponent } from './rgw-sync-data-info/rgw-sync-data-inf import { BucketTagModalComponent } from './bucket-tag-modal/bucket-tag-modal.component'; import { NfsListComponent } from '../nfs/nfs-list/nfs-list.component'; import { NfsFormComponent } from '../nfs/nfs-form/nfs-form.component'; +import { RgwMultisiteSyncPolicyComponent } from './rgw-multisite-sync-policy/rgw-multisite-sync-policy.component'; @NgModule({ imports: [ @@ -106,8 +107,10 @@ import { NfsFormComponent } from '../nfs/nfs-form/nfs-form.component'; RgwSyncPrimaryZoneComponent, RgwSyncMetadataInfoComponent, RgwSyncDataInfoComponent, - BucketTagModalComponent - ] + BucketTagModalComponent, + RgwMultisiteSyncPolicyComponent + ], + providers: [TitleCasePipe] }) export class RgwModule {} 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 new file mode 100644 index 0000000000000..3f5b3d3b57284 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.spec.ts @@ -0,0 +1,50 @@ +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { configureTestBed } from '~/testing/unit-test-helper'; +import { RgwMultisiteService } from './rgw-multisite.service'; + +const mockSyncPolicyData: any = [ + { + id: 'test', + data_flow: {}, + pipes: [], + status: 'enabled', + bucketName: 'test' + }, + { + id: 'test', + data_flow: {}, + pipes: [], + status: 'enabled' + } +]; + +describe('RgwMultisiteService', () => { + let service: RgwMultisiteService; + let httpTesting: HttpTestingController; + + configureTestBed({ + providers: [RgwMultisiteService], + imports: [HttpClientTestingModule] + }); + + beforeEach(() => { + service = TestBed.inject(RgwMultisiteService); + httpTesting = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpTesting.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + 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'); + expect(req.request.method).toBe('GET'); + req.flush(mockSyncPolicyData); + }); +}); 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 9081c21e44003..4f69d6ab2a4c1 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 @@ -34,4 +34,17 @@ export class RgwMultisiteService { status() { return this.http.get(`${this.uiUrl}/status`); } + + 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 }); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html index f977273b0cf64..a856a4c487019 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html @@ -317,6 +317,20 @@ {{ value | map:column?.customTemplateConfig }} + + + + {{value}} + + + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts index 6e39f4bff138e..905646b55b8a8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts @@ -75,6 +75,8 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O rowSelectionTpl: TemplateRef; @ViewChild('pathTpl', { static: true }) pathTpl: TemplateRef; + @ViewChild('tooltipTpl', { static: true }) + tooltipTpl: TemplateRef; // This is the array with the items to be shown. @Input() @@ -612,6 +614,7 @@ export class TableComponent implements AfterContentChecked, OnInit, OnChanges, O this.cellTemplates.truncate = this.truncateTpl; this.cellTemplates.timeAgo = this.timeAgoTpl; this.cellTemplates.path = this.pathTpl; + this.cellTemplates.tooltip = this.tooltipTpl; } useCustomClass(value: any): string { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts index 2790f97497859..5c4072f7f1fc6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts @@ -60,5 +60,17 @@ export enum CellTemplate { This template truncates a path to a shorter format and shows the whole path in a tooltip eg: /var/lib/ceph/osd/ceph-0 -> /var/.../ceph-0 */ - path = 'path' + path = 'path', + /* + This template is used to attach tooltip to the given column value + // { + // ... + // cellTransformation: CellTemplate.tooltip, + // customTemplateConfig: { + // map?: { + // [key: any]: { class?: string, tooltip: string } + // } + // } + */ + tooltip = 'tooltip' } diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 7593bbe5fc255..e893b0e664bc5 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -10928,6 +10928,11 @@ paths: name: zonegroup_name schema: type: string + - allowEmptyValue: true + in: query + name: all_policy + schema: + type: string responses: '200': content: