From: Naman Munet Date: Thu, 11 Jul 2024 18:40:51 +0000 (+0530) Subject: mgr/dashboard: RGW multisite sync pipe X-Git-Tag: testing/wip-xiubli-testing-20240726.021939-squid~42^2 X-Git-Url: http://git.apps.os.sepia.ceph.com/?a=commitdiff_plain;h=4bc81fe3c41543dd662991a6842bcbae9643f287;p=ceph-ci.git mgr/dashboard: RGW multisite sync pipe Fixes: https://tracker.ceph.com/issues/66926 Signed-off-by: Naman Munet (cherry picked from commit d451b4d1795e1429b0a530940558cf646579cdb9) --- diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index e265d2e6bae..03d8cdc8cf4 100644 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -200,14 +200,15 @@ class RgwMultisiteController(RESTController): @EndpointDoc("Create or update the sync pipe") @CreatePermission def create_sync_pipe(self, group_id: str, pipe_id: str, + source_bucket: str = '', source_zones: Optional[List[str]] = None, destination_zones: Optional[List[str]] = None, - destination_buckets: Optional[List[str]] = None, + destination_bucket: str = '', bucket_name: str = ''): multisite_instance = RgwMultisite() return multisite_instance.create_sync_pipe(group_id, pipe_id, source_zones, - destination_zones, destination_buckets, - bucket_name) + destination_zones, source_bucket, + destination_bucket, bucket_name) @Endpoint(method='DELETE', path='/sync-pipe') @EndpointDoc("Remove the sync pipe") @@ -215,11 +216,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, - destination_buckets: Optional[List[str]] = None, + destination_bucket: str = '', bucket_name: str = ''): multisite_instance = RgwMultisite() return multisite_instance.remove_sync_pipe(group_id, pipe_id, source_zones, - destination_zones, destination_buckets, + destination_zones, destination_bucket, bucket_name) diff --git a/src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts b/src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts index 6a7eb2a802d..35f34a34b4b 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/page-helper.po.ts @@ -308,31 +308,31 @@ export abstract class PageHelper { } getNestedTableCell( - tableSelector: string, + selector: string, columnIndex: number, exactContent: string, partialMatch = false ) { this.waitDataTableToLoad(); this.clearTableSearchInput(); - this.searchNestedTable(tableSelector, exactContent); + this.searchNestedTable(selector, exactContent); if (partialMatch) { return cy - .get(`${tableSelector} datatable-body-row datatable-body-cell:nth-child(${columnIndex})`) + .get(`${selector} datatable-body-row datatable-body-cell:nth-child(${columnIndex})`) .should('contain', exactContent); } return cy - .get(`${tableSelector}`) + .get(`${selector}`) .contains( `datatable-body-row datatable-body-cell:nth-child(${columnIndex})`, new RegExp(`^${exactContent}$`) ); } - searchNestedTable(tableSelector: string, text: string) { + searchNestedTable(selector: string, text: string) { this.waitDataTableToLoad(); this.setPageSize('10'); - cy.get(`${tableSelector} [aria-label=search]`).first().clear({ force: true }).type(text); + cy.get(`${selector} [aria-label=search]`).first().clear({ force: true }).type(text); } } 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 index e884ec624e3..a0306f126b3 100644 --- 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 @@ -79,4 +79,23 @@ describe('Multisite page', () => { ); }); }); + + describe('create, edit, delete pipe', () => { + beforeEach(() => { + multisite.getTab('Sync Policy').click(); + multisite.getExpandCollapseElement().click(); + }); + + it('should create pipe', () => { + multisite.createPipe('new-pipe', ['zone1-zg1-realm1'], ['zone3-zg2-realm1']); + }); + + it('should modify pipe zones', () => { + multisite.editPipe('new-pipe', 'zone2-zg1-realm1'); + }); + + it('should delete pipe', () => { + multisite.deletePipe('new-pipe'); + }); + }); }); 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 index d912f6b1ca0..11a43a7b8b3 100644 --- a/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/multisite.po.ts +++ b/src/pybind/mgr/dashboard/frontend/cypress/e2e/rgw/multisite.po.ts @@ -185,4 +185,108 @@ export class MultisitePageHelper extends PageHelper { .find('.datatable-body-cell-label') .should('contain', dest_zones[0]); } + + @PageHelper.restrictTo(pages.index.url) + createPipe(pipe_id: string, source_zones: string[], dest_zones: string[]) { + cy.get('cd-rgw-multisite-sync-policy-details').should('exist'); + this.getTab('Pipe').should('exist'); + this.getTab('Pipe').click(); + cy.request({ + method: 'GET', + url: '/api/rgw/daemon', + headers: { Accept: 'application/vnd.ceph.api.v1.0+json' } + }); + cy.get('cd-rgw-multisite-sync-policy-details .table-actions button').first().click(); + cy.get('cd-rgw-multisite-sync-pipe-modal').should('exist'); + + // Enter in pipe_id + cy.get('#pipe_id').type(pipe_id); + cy.wait(WAIT_TIMER); + // Select zone + cy.get('a[data-testid=select-menu-edit]').eq(0).click(); + for (const zone of source_zones) { + cy.get('.popover-body div.select-menu-item-content').contains(zone).click(); + } + cy.get('cd-rgw-multisite-sync-pipe-modal').click(); + cy.get('a[data-testid=select-menu-edit]').eq(1).click(); + for (const zone of dest_zones) { + cy.get('.popover-body input').type(`${zone}{enter}`); + } + cy.get('button.tc_submitButton').click(); + + cy.get('cd-rgw-multisite-sync-policy-details .datatable-body-cell-label').should( + 'contain', + pipe_id + ); + + cy.get('cd-rgw-multisite-sync-policy-details') + .first() + .find('[aria-label=search]') + .first() + .clear({ force: true }) + .type(pipe_id); + } + + @PageHelper.restrictTo(pages.index.url) + editPipe(pipe_id: string, zoneToAdd: string) { + cy.get('cd-rgw-multisite-sync-policy-details').should('exist'); + this.getTab('Pipe').should('exist'); + this.getTab('Pipe').click(); + cy.request({ + method: 'GET', + url: '/api/rgw/daemon', + headers: { Accept: 'application/vnd.ceph.api.v1.0+json' } + }); + + cy.get('cd-rgw-multisite-sync-policy-details').within(() => { + cy.get('.datatable-body-cell-label').should('contain', pipe_id); + cy.get('[aria-label=search]').first().clear({ force: true }).type(pipe_id); + cy.get('input.cd-datatable-checkbox').first().check(); + cy.get('.table-actions button').first().click(); + }); + cy.get('cd-rgw-multisite-sync-pipe-modal').should('exist'); + + cy.wait(WAIT_TIMER); + // Enter in pipe_id + cy.get('#pipe_id').should('contain.value', pipe_id); + // Select zone + cy.get('a[data-testid=select-menu-edit]').eq(1).click(); + + cy.get('.popover-body input').type(`${zoneToAdd}{enter}`); + + cy.get('button.tc_submitButton').click(); + + this.getNestedTableCell('cd-rgw-multisite-sync-policy-details', 4, zoneToAdd, true); + } + + @PageHelper.restrictTo(pages.index.url) + deletePipe(pipe_id: string) { + cy.get('cd-rgw-multisite-sync-policy-details').should('exist'); + this.getTab('Pipe').should('exist'); + this.getTab('Pipe').click(); + cy.get('cd-rgw-multisite-sync-policy-details').within(() => { + cy.get('.datatable-body-cell-label').should('contain', pipe_id); + cy.get('[aria-label=search]').first().clear({ force: true }).type(pipe_id); + }); + + const getRow = this.getTableCellWithContent.bind(this); + getRow('cd-rgw-multisite-sync-policy-details', pipe_id).click(); + + cy.get('cd-rgw-multisite-sync-policy-details').within(() => { + cy.get('.table-actions button.dropdown-toggle').first().click(); // open submenu + cy.get(`button.delete`).first().click(); + }); + + cy.get('cd-modal .custom-control-label').click(); + cy.get('[aria-label="Delete Pipe"]').click(); + cy.get('cd-modal').should('not.exist'); + + cy.get('cd-rgw-multisite-sync-policy-details') + .first() + .within(() => { + cy.get('[aria-label=search]').first().clear({ force: true }).type(pipe_id); + }); + // Waits for item to be removed from table + getRow(pipe_id).should('not.exist'); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.html index b6f8e3ec4e5..974474c5938 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.html @@ -38,8 +38,7 @@ type="text" i18n-placeholder placeholder="Bucket Name..." - formControlName="bucket_name" - [readonly]="true"/> + formControlName="bucket_name"/> { - this.notificationService.show( - NotificationType.success, - $localize`Created Sync Flow '${this.currentFormGroupContext.getValue('flow_id')}'` - ); - this.activeModal.close('success'); - }, - () => { - // Reset the 'Submit' button. - this.currentFormGroupContext.setErrors({ cdSubmitButton: true }); - this.activeModal.dismiss(); - } - ); + this.rgwMultisiteService + .createEditSyncFlow(this.currentFormGroupContext.getRawValue()) + .subscribe( + () => { + const action = this.editing ? 'Modified' : 'Created'; + this.notificationService.show( + NotificationType.success, + $localize`${action} Sync Flow '${this.currentFormGroupContext.getValue('flow_id')}'` + ); + this.activeModal.close('success'); + }, + () => { + // Reset the 'Submit' button. + this.currentFormGroupContext.setErrors({ cdSubmitButton: true }); + this.activeModal.dismiss(); + } + ); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.html new file mode 100644 index 00000000000..98f3e10bded --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.html @@ -0,0 +1,132 @@ + + {{ action | titlecase }} Pipe + + +
+ + +
+
+
+ + + + + + + {{ name }} selection is required! + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.spec.ts new file mode 100644 index 00000000000..30fd3e42c0e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.spec.ts @@ -0,0 +1,36 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RgwMultisiteSyncPipeModalComponent } from './rgw-multisite-sync-pipe-modal.component'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ToastrModule } from 'ngx-toastr'; +import { PipesModule } from '~/app/shared/pipes/pipes.module'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +describe('RgwMultisiteSyncPipeModalComponent', () => { + let component: RgwMultisiteSyncPipeModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RgwMultisiteSyncPipeModalComponent], + imports: [ + HttpClientTestingModule, + ToastrModule.forRoot(), + PipesModule, + ReactiveFormsModule, + CommonModule + ], + providers: [NgbActiveModal] + }).compileComponents(); + + fixture = TestBed.createComponent(RgwMultisiteSyncPipeModalComponent); + 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-pipe-modal/rgw-multisite-sync-pipe-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.ts new file mode 100644 index 00000000000..29d32ea967a --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component.ts @@ -0,0 +1,137 @@ +import { Component, OnInit } from '@angular/core'; +import { UntypedFormControl, Validators } from '@angular/forms'; +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { RgwZonegroup } from '../models/rgw-multisite'; +import { SelectOption } from '~/app/shared/components/select/select-option.model'; +import { catchError, switchMap } from 'rxjs/operators'; +import { of } from 'rxjs'; +import { RgwDaemon } from '../models/rgw-daemon'; +import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service'; +import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import _ from 'lodash'; +import { Icons } from '~/app/shared/enum/icons.enum'; +import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service'; +import { NotificationType } from '~/app/shared/enum/notification-type.enum'; +import { NotificationService } from '~/app/shared/services/notification.service'; +import { ZoneData } from '../models/rgw-multisite-zone-selector'; + +@Component({ + selector: 'cd-rgw-multisite-sync-pipe-modal', + templateUrl: './rgw-multisite-sync-pipe-modal.component.html', + styleUrls: ['./rgw-multisite-sync-pipe-modal.component.scss'] +}) +export class RgwMultisiteSyncPipeModalComponent implements OnInit { + groupExpandedRow: any; + pipeSelectedRow: any; + pipeForm: CdFormGroup; + action: string; + editing: boolean; + sourceZones = new ZoneData(false, 'Filter Zones'); + destZones = new ZoneData(true, 'Filter or Add Zones'); + icons = Icons; + + constructor( + public activeModal: NgbActiveModal, + private rgwDaemonService: RgwDaemonService, + private rgwZonegroupService: RgwZonegroupService, + private rgwMultisiteService: RgwMultisiteService, + private notificationService: NotificationService + ) {} + + ngOnInit(): void { + this.editing = this.action === 'create' ? false : true; + this.pipeForm = new CdFormGroup({ + pipe_id: new UntypedFormControl('', { + validators: [Validators.required] + }), + group_id: new UntypedFormControl(this.groupExpandedRow?.groupName || '', { + validators: [Validators.required] + }), + bucket_name: new UntypedFormControl(this.groupExpandedRow?.bucket || ''), + source_bucket: new UntypedFormControl(''), + source_zones: new UntypedFormControl('', { + validators: [Validators.required] + }), + destination_bucket: new UntypedFormControl(''), + destination_zones: new UntypedFormControl('', { + validators: [Validators.required] + }) + }); + this.pipeForm.get('bucket_name').disable(); + this.rgwDaemonService.selectedDaemon$ + .pipe( + switchMap((daemon: RgwDaemon) => { + if (daemon) { + const zonegroupObj = new RgwZonegroup(); + zonegroupObj.name = daemon.zonegroup_name; + return this.rgwZonegroupService.get(zonegroupObj).pipe( + catchError(() => { + return of([]); + }) + ); + } else { + return of([]); + } + }) + ) + .subscribe((zonegroupData: any) => { + if (zonegroupData && zonegroupData?.zones?.length > 0) { + let zones: any[] = []; + zonegroupData.zones.forEach((zone: any) => { + zones.push(new SelectOption(false, zone.name, '')); + }); + this.sourceZones.data.available = [...zones]; + if (this.editing) { + this.sourceZones.data.selected = this.pipeSelectedRow.source.zones; + this.destZones.data.selected = this.pipeSelectedRow.dest.zones; + this.pipeForm.patchValue({ + pipe_id: this.pipeSelectedRow.id, + source_zones: this.pipeSelectedRow.source.zones, + destination_zones: this.pipeSelectedRow.dest.zones, + source_bucket: this.pipeSelectedRow.source.bucket, + destination_bucket: this.pipeSelectedRow.dest.bucket + }); + } + } + }); + } + + onZoneSelection(zoneType: string) { + if (zoneType === 'source_zones') { + this.pipeForm.patchValue({ + source_zones: this.sourceZones.data.selected + }); + } else { + this.pipeForm.patchValue({ + destination_zones: this.destZones.data.selected + }); + } + } + + submit() { + if (this.pipeForm.invalid) { + return; + } + // Ensure that no validation is pending + if (this.pipeForm.pending) { + this.pipeForm.setErrors({ cdSubmitButton: true }); + return; + } + this.rgwMultisiteService.createEditSyncPipe(this.pipeForm.getRawValue()).subscribe( + () => { + const action = this.editing ? 'Modified' : 'Created'; + this.notificationService.show( + NotificationType.success, + $localize`${action} Sync Pipe '${this.pipeForm.getValue('pipe_id')}'` + ); + this.activeModal.close('success'); + }, + () => { + // Reset the 'Submit' button. + this.pipeForm.setErrors({ cdSubmitButton: true }); + this.activeModal.dismiss(); + } + ); + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.html index 5fabf9d6bcb..b9f16ab4d3c 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.html @@ -29,7 +29,7 @@ [maxLimit]="25" [toolHeader]="true" (updateSelection)="updateSelection($event, flowType.symmetrical)" - (fetchData)="loadFlowData($event)"> + (fetchData)="loadData($event)">
+ (fetchData)="loadData($event)">
- 'Edit' and 'Delete' functionalities for Directional flow are disabled for now due to some internal dependency. They will be enabled once the issue is resolved. + title="Directional Flow 'edit' & 'delete' actions disabled" + i18n-title + i18n> + Due to some internal dependencies these actions are disabled, it will get enabled once the issue gets resolved. + + Pipe + + + Pipe + + A pipe defines the actual buckets that can use these data flows, and the properties that are associated with it. + + + +
+ + +
+
+
+
-
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.ts index 14cc7a7f02f..b93c4ae788e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.ts @@ -15,6 +15,7 @@ import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { TableComponent } from '~/app/shared/datatable/table/table.component'; import { RgwMultisiteSyncFlowModalComponent } from '../rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component'; import { FlowType } from '../models/rgw-multisite'; +import { RgwMultisiteSyncPipeModalComponent } from '../rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component'; @Component({ selector: 'cd-rgw-multisite-sync-policy-details', @@ -36,12 +37,16 @@ export class RgwMultisiteSyncPolicyDetailsComponent implements OnChanges { modalRef: NgbModalRef; symmetricalFlowData: any = []; directionalFlowData: any = []; + pipeData: any = []; symmetricalFlowCols: CdTableColumn[]; directionalFlowCols: CdTableColumn[]; + pipeCols: CdTableColumn[]; symFlowTableActions: CdTableAction[]; dirFlowTableActions: CdTableAction[]; + pipeTableActions: CdTableAction[]; symFlowSelection = new CdTableSelection(); dirFlowSelection = new CdTableSelection(); + pipeSelection = new CdTableSelection(); constructor( private actionLabels: ActionLabelsI18n, @@ -73,6 +78,33 @@ export class RgwMultisiteSyncPolicyDetailsComponent implements OnChanges { flexGrow: 1 } ]; + this.pipeCols = [ + { + name: 'Name', + prop: 'id', + flexGrow: 1 + }, + { + name: 'Source Zone', + prop: 'source.zones', + flexGrow: 1 + }, + { + name: 'Destination Zone', + prop: 'dest.zones', + flexGrow: 1 + }, + { + name: 'Source Bucket', + prop: 'source.bucket', + flexGrow: 1 + }, + { + name: 'Destination Bucket', + prop: 'dest.bucket', + flexGrow: 1 + } + ]; const symAddAction: CdTableAction = { permission: 'create', icon: Icons.add, @@ -118,17 +150,39 @@ export class RgwMultisiteSyncPolicyDetailsComponent implements OnChanges { canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection }; this.dirFlowTableActions = [dirAddAction, dirEditAction, dirDeleteAction]; + const pipeAddAction: CdTableAction = { + permission: 'create', + icon: Icons.add, + name: this.actionLabels.CREATE, + click: () => this.openPipeModal(), + canBePrimary: (selection: CdTableSelection) => !selection.hasSelection + }; + const pipeEditAction: CdTableAction = { + permission: 'update', + icon: Icons.edit, + name: this.actionLabels.EDIT, + click: () => this.openPipeModal(true) + }; + const pipeDeleteAction: CdTableAction = { + permission: 'delete', + icon: Icons.destroy, + disable: () => !this.pipeSelection.hasSelection, + name: this.actionLabels.DELETE, + click: () => this.deletePipe(), + canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection + }; + this.pipeTableActions = [pipeAddAction, pipeEditAction, pipeDeleteAction]; } ngOnChanges(changes: SimpleChanges): void { if (changes.expandedRow.currentValue && changes.expandedRow.currentValue.groupName) { this.symmetricalFlowData = []; this.directionalFlowData = []; - this.loadFlowData(); + this.loadData(); } } - loadFlowData(context?: any) { + loadData(context?: any) { if (this.expandedRow) { this.rgwMultisiteService .getSyncPolicyGroup(this.expandedRow.groupName, this.expandedRow.bucket) @@ -136,6 +190,7 @@ export class RgwMultisiteSyncPolicyDetailsComponent implements OnChanges { (policy: any) => { this.symmetricalFlowData = policy.data_flow[FlowType.symmetrical] || []; this.directionalFlowData = policy.data_flow[FlowType.directional] || []; + this.pipeData = policy.pipes || []; }, () => { if (context) { @@ -173,7 +228,7 @@ export class RgwMultisiteSyncPolicyDetailsComponent implements OnChanges { try { const res = await this.modalRef.result; if (res === 'success') { - this.loadFlowData(); + this.loadData(); } } catch (err) {} } @@ -225,4 +280,67 @@ export class RgwMultisiteSyncPolicyDetailsComponent implements OnChanges { } }); } + + async openPipeModal(edit = false) { + const action = edit ? 'edit' : 'create'; + const initialState = { + groupExpandedRow: this.expandedRow, + pipeSelectedRow: this.pipeSelection.first(), + action: action + }; + + this.modalRef = this.modalService.show(RgwMultisiteSyncPipeModalComponent, initialState, { + size: 'lg' + }); + + try { + const res = await this.modalRef.result; + if (res === 'success') { + this.loadData(); + } + } catch (err) {} + } + + deletePipe() { + const pipeIds = this.pipeSelection.selected.map((pipe: any) => pipe.id); + this.modalService.show(CriticalConfirmationModalComponent, { + itemDescription: this.pipeSelection.hasSingleSelection ? $localize`Pipe` : $localize`Pipes`, + itemNames: pipeIds, + bodyTemplate: this.deleteTpl, + submitActionObservable: () => { + return new Observable((observer: Subscriber) => { + this.taskWrapper + .wrapTaskAroundCall({ + task: new FinishedTask('rgw/multisite/sync-pipe/delete', { + pipe_ids: pipeIds + }), + call: observableForkJoin( + this.pipeSelection.selected.map((pipe: any) => { + return this.rgwMultisiteService.removeSyncPipe( + pipe.id, + this.expandedRow.groupName, + this.expandedRow.bucket + ); + }) + ) + }) + .subscribe({ + error: (error: any) => { + // Forward the error to the observer. + observer.error(error); + // Reload the data table content because some deletions might + // have been executed successfully in the meanwhile. + this.table.refreshBtn(); + }, + complete: () => { + // Notify the observer that we are done. + observer.complete(); + // Reload the data table content. + this.table.refreshBtn(); + } + }); + }); + } + }); + } } 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 9c5cb55d2bd..accd83fc738 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 @@ -58,6 +58,7 @@ import { RgwMultisiteSyncPolicyComponent } from './rgw-multisite-sync-policy/rgw import { RgwMultisiteSyncPolicyFormComponent } from './rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component'; import { RgwMultisiteSyncPolicyDetailsComponent } from './rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component'; import { RgwMultisiteSyncFlowModalComponent } from './rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component'; +import { RgwMultisiteSyncPipeModalComponent } from './rgw-multisite-sync-pipe-modal/rgw-multisite-sync-pipe-modal.component'; @NgModule({ imports: [ @@ -120,7 +121,8 @@ import { RgwMultisiteSyncFlowModalComponent } from './rgw-multisite-sync-flow-mo RgwMultisiteSyncPolicyComponent, RgwMultisiteSyncPolicyFormComponent, RgwMultisiteSyncPolicyDetailsComponent, - RgwMultisiteSyncFlowModalComponent + RgwMultisiteSyncFlowModalComponent, + RgwMultisiteSyncPipeModalComponent ], providers: [TitleCasePipe] }) 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 cdc85b25b9f..01e4ccb9945 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 @@ -169,4 +169,43 @@ describe('RgwMultisiteService', () => { expect(req.request.method).toBe('DELETE'); req.flush(null); }); + + it('should create Sync Pipe', () => { + const payload = { + pipe_id: 'test', + bucket_name: 'test', + source_zones: ['zone1-zg1-realm1'], + destination_zones: ['zone1-zg2-realm2'], + group_id: 'sync-grp' + }; + service.createEditSyncPipe(payload).subscribe(); + const req = httpTesting.expectOne('api/rgw/multisite/sync-pipe'); + expect(req.request.method).toBe('PUT'); + expect(req.request.body).toEqual(payload); + req.flush(null); + }); + + it('should edit Symmetrical Sync flow', () => { + const payload = { + pipe_id: 'test', + bucket_name: 'test', + source_zones: ['zone1-zg1-realm1'], + destination_zones: ['zone1-zg2-realm2', 'zone2-zg1-realm1'], + group_id: 'sync-grp' + }; + service.createEditSyncFlow(payload).subscribe(); + const req = httpTesting.expectOne('api/rgw/multisite/sync-flow'); + expect(req.request.method).toBe('PUT'); + expect(req.request.body).toEqual(payload); + req.flush(null); + }); + + 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` + ); + 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 41110333531..0048abe3810 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 @@ -88,4 +88,19 @@ export class RgwMultisiteService { { params } ); } + + createEditSyncPipe(payload: any) { + return this.http.put(`${this.url}/sync-pipe`, payload); + } + + 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 } + ); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts index 399003b1cb6..a3e973da6bd 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts @@ -332,9 +332,15 @@ export class TaskMessageService { this.commonOperations.delete, (metadata) => { return $localize`${ - metadata.flow_ids.length > 1 - ? 'selected Flow Names' - : `Flow Name '${metadata.flow_ids[0]}'` + metadata.flow_ids.length > 1 ? 'selected Flow' : `Flow '${metadata.flow_ids[0]}'` + }`; + } + ), + 'rgw/multisite/sync-pipe/delete': this.newTaskMessage( + this.commonOperations.delete, + (metadata) => { + return $localize`${ + metadata.pipe_ids.length > 1 ? 'selected pipe' : `Pipe '${metadata.pipe_ids[0]}'` }`; } ), diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 9f9167ef4c4..c3292e8c7e5 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -10816,7 +10816,8 @@ paths: bucket_name: default: '' type: string - destination_buckets: + destination_bucket: + default: '' type: string destination_zones: type: string @@ -10824,6 +10825,9 @@ paths: type: string pipe_id: type: string + source_bucket: + default: '' + type: string source_zones: type: string required: @@ -10878,9 +10882,9 @@ paths: name: destination_zones schema: type: string - - allowEmptyValue: true + - default: '' in: query - name: destination_buckets + name: destination_bucket schema: type: string - default: '' diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py index cd177e8065b..dfac109edc7 100644 --- a/src/pybind/mgr/dashboard/services/rgw_client.py +++ b/src/pybind/mgr/dashboard/services/rgw_client.py @@ -1948,7 +1948,9 @@ class RgwMultisite: def create_sync_pipe(self, group_id: str, pipe_id: str, source_zones: Optional[List[str]] = None, destination_zones: Optional[List[str]] = None, - destination_buckets: Optional[List[str]] = None, bucket_name: str = ''): + source_bucket: str = '', + destination_bucket: str = '', + bucket_name: str = ''): rgw_sync_policy_cmd = ['sync', 'group', 'pipe', 'create', '--group-id', group_id, '--pipe-id', pipe_id] @@ -1961,8 +1963,11 @@ class RgwMultisite: if destination_zones: rgw_sync_policy_cmd += ['--dest-zones', ','.join(destination_zones)] - if destination_buckets: - rgw_sync_policy_cmd += ['--dest-bucket', ','.join(destination_buckets)] + if source_bucket: + rgw_sync_policy_cmd += ['--source-bucket', source_bucket] + + if destination_bucket: + rgw_sync_policy_cmd += ['--dest-bucket', destination_bucket] try: exit_code, _, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd) @@ -1975,7 +1980,7 @@ class RgwMultisite: def remove_sync_pipe(self, group_id: str, pipe_id: str, source_zones: Optional[List[str]] = None, destination_zones: Optional[List[str]] = None, - destination_buckets: Optional[List[str]] = None, bucket_name: str = ''): + destination_bucket: str = '', bucket_name: str = ''): rgw_sync_policy_cmd = ['sync', 'group', 'pipe', 'remove', '--group-id', group_id, '--pipe-id', pipe_id] @@ -1988,8 +1993,8 @@ class RgwMultisite: if destination_zones: rgw_sync_policy_cmd += ['--dest-zones', ','.join(destination_zones)] - if destination_buckets: - rgw_sync_policy_cmd += ['--dest-bucket', ','.join(destination_buckets)] + if destination_bucket: + rgw_sync_policy_cmd += ['--dest-bucket', destination_bucket] try: exit_code, _, err = mgr.send_rgwadmin_command(rgw_sync_policy_cmd) @@ -2014,7 +2019,7 @@ class RgwMultisite: zones=zone_names) # create a sync pipe with source and destination zones self.create_sync_pipe(_SYNC_GROUP_ID, _SYNC_PIPE_ID, source_zones=['*'], - destination_zones=['*'], destination_buckets=['*']) + destination_zones=['*'], source_bucket='*', destination_bucket='*') # period update --commit self.update_period()