From 41d46b7b507f95603ec432c061fdf0569f12dc3b Mon Sep 17 00:00:00 2001 From: Naman Munet Date: Fri, 12 Jul 2024 00:10:51 +0530 Subject: [PATCH] mgr/dashboard: RGW multisite sync flow Fixes: https://tracker.ceph.com/issues/66915 Signed-off-by: Naman Munet --- src/pybind/mgr/dashboard/controllers/rgw.py | 4 +- .../frontend/cypress/e2e/page-helper.po.ts | 29 +++ .../cypress/e2e/rgw/multisite.e2e.spec.ts | 46 +++- .../frontend/cypress/e2e/rgw/multisite.po.ts | 148 +++++++++++- .../rgw/models/rgw-multisite-zone-selector.ts | 33 +++ .../src/app/ceph/rgw/models/rgw-multisite.ts | 5 + ...w-multisite-sync-flow-modal.component.html | 121 ++++++++++ ...w-multisite-sync-flow-modal.component.scss | 0 ...ultisite-sync-flow-modal.component.spec.ts | 40 +++ ...rgw-multisite-sync-flow-modal.component.ts | 179 ++++++++++++++ ...ltisite-sync-policy-details.component.html | 89 +++++++ ...ltisite-sync-policy-details.component.scss | 3 + ...site-sync-policy-details.component.spec.ts | 26 ++ ...multisite-sync-policy-details.component.ts | 228 ++++++++++++++++++ .../rgw-multisite-sync-policy.component.html | 15 +- .../rgw-multisite-sync-policy.component.ts | 7 +- .../frontend/src/app/ceph/rgw/rgw.module.ts | 6 +- .../shared/api/rgw-multisite.service.spec.ts | 80 ++++++ .../app/shared/api/rgw-multisite.service.ts | 17 ++ .../shared/services/task-message.service.ts | 10 + src/pybind/mgr/dashboard/openapi.yaml | 2 - .../mgr/dashboard/services/rgw_client.py | 8 +- 22 files changed, 1076 insertions(+), 20 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-selector.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.ts diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index c479214dce365..014d9eb953b79 100644 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -200,7 +200,9 @@ class RgwMultisiteController(RESTController): @EndpointDoc("Create or update the sync flow") @CreatePermission def create_sync_flow(self, flow_id: str, flow_type: str, group_id: str, - source_zone='', destination_zone='', zones: Optional[List[str]] = None, + source_zone: Optional[List[str]] = None, + destination_zone: Optional[List[str]] = None, + zones: Optional[List[str]] = None, bucket_name=''): multisite_instance = RgwMultisite() return multisite_instance.create_sync_flow(group_id, flow_id, flow_type, zones, 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 49144b25fbfc9..6a7eb2a802d7f 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 @@ -306,4 +306,33 @@ export abstract class PageHelper { // Waits for item to be removed from table getRow(name).should('not.exist'); } + + getNestedTableCell( + tableSelector: string, + columnIndex: number, + exactContent: string, + partialMatch = false + ) { + this.waitDataTableToLoad(); + this.clearTableSearchInput(); + this.searchNestedTable(tableSelector, exactContent); + if (partialMatch) { + return cy + .get(`${tableSelector} datatable-body-row datatable-body-cell:nth-child(${columnIndex})`) + .should('contain', exactContent); + } + return cy + .get(`${tableSelector}`) + .contains( + `datatable-body-row datatable-body-cell:nth-child(${columnIndex})`, + new RegExp(`^${exactContent}$`) + ); + } + + searchNestedTable(tableSelector: string, text: string) { + this.waitDataTableToLoad(); + + this.setPageSize('10'); + cy.get(`${tableSelector} [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 5633bb2f5b4b8..e884ec624e34d 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 @@ -20,11 +20,6 @@ describe('Multisite page', () => { 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'); - }); }); describe('create, edit & delete sync group policy', () => { @@ -43,4 +38,45 @@ describe('Multisite page', () => { multisite.delete('test'); }); }); + + describe('create, edit & delete symmetrical sync Flow', () => { + it('Preparing...(creating sync group policy)', () => { + multisite.navigateTo('create'); + multisite.create('test', 'Enabled'); + multisite.getFirstTableCell('test').should('exist'); + }); + describe('symmetrical Flow creation started', () => { + beforeEach(() => { + multisite.getTab('Sync Policy').click(); + multisite.getExpandCollapseElement().click(); + }); + + it('should create flow', () => { + multisite.createSymmetricalFlow('new-sym-flow', ['zone1-zg1-realm1']); + }); + + it('should modify flow zones', () => { + multisite.editSymFlow('new-sym-flow', 'zone2-zg1-realm1'); + }); + + it('should delete flow', () => { + multisite.deleteSymFlow('new-sym-flow'); + }); + }); + }); + + describe('create, edit & delete directional sync Flow', () => { + beforeEach(() => { + multisite.getTab('Sync Policy').click(); + multisite.getExpandCollapseElement().click(); + }); + + it('should create flow', () => { + multisite.createDirectionalFlow( + 'new-dir-flow', + ['zone1-zg1-realm1', 'zone2-zg1-realm1'], + ['new-zone'] + ); + }); + }); }); 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 bbeda74e9cfac..d912f6b1ca024 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 @@ -10,7 +10,7 @@ export class MultisitePageHelper extends PageHelper { pages = pages; columnIndex = { - status: 3 + status: 4 }; @PageHelper.restrictTo(pages.create.url) @@ -39,4 +39,150 @@ export class MultisitePageHelper extends PageHelper { .find('.badge-warning') .should('contain', status); } + + @PageHelper.restrictTo(pages.index.url) + createSymmetricalFlow(flow_id: string, zones: string[]) { + cy.get('cd-rgw-multisite-sync-policy-details').should('exist'); + this.getTab('Flow').should('exist'); + this.getTab('Flow').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-flow-modal').should('exist'); + + // Enter in flow_id + cy.get('#flow_id').type(flow_id); + // Select zone + cy.get('a[data-testid=select-menu-edit]').click(); + for (const zone of zones) { + cy.get('.popover-body div.select-menu-item-content').contains(zone).click(); + } + + cy.get('button.tc_submitButton').click(); + + cy.get('cd-rgw-multisite-sync-policy-details .datatable-body-cell-label').should( + 'contain', + flow_id + ); + + cy.get('cd-rgw-multisite-sync-policy-details') + .first() + .find('[aria-label=search]') + .first() + .clear({ force: true }) + .type(flow_id); + } + + @PageHelper.restrictTo(pages.index.url) + editSymFlow(flow_id: string, zoneToAdd: string) { + cy.get('cd-rgw-multisite-sync-policy-details').should('exist'); + this.getTab('Flow').should('exist'); + this.getTab('Flow').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', flow_id); + cy.get('[aria-label=search]').first().clear({ force: true }).type(flow_id); + cy.get('input.cd-datatable-checkbox').first().check(); + cy.get('.table-actions button').first().click(); + }); + cy.get('cd-rgw-multisite-sync-flow-modal').should('exist'); + + // Enter in flow_id + cy.get('#flow_id').wait(100).should('contain.value', flow_id); + // Select zone + cy.get('a[data-testid=select-menu-edit]').click(); + + cy.get('.popover-body div.select-menu-item-content').contains(zoneToAdd).click(); + + cy.get('button.tc_submitButton').click(); + + this.getNestedTableCell('cd-rgw-multisite-sync-policy-details', 3, zoneToAdd, true); + } + + getTableCellWithContent(nestedClass: string, content: string) { + return cy.contains(`${nestedClass} .datatable-body-cell-label`, content); + } + + @PageHelper.restrictTo(pages.index.url) + deleteSymFlow(flow_id: string) { + cy.get('cd-rgw-multisite-sync-policy-details').should('exist'); + this.getTab('Flow').should('exist'); + this.getTab('Flow').click(); + cy.get('cd-rgw-multisite-sync-policy-details').within(() => { + cy.get('.datatable-body-cell-label').should('contain', flow_id); + cy.get('[aria-label=search]').first().clear({ force: true }).type(flow_id); + }); + + const getRow = this.getTableCellWithContent.bind(this); + getRow('cd-rgw-multisite-sync-policy-details', flow_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 Flow"]').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(flow_id); + }); + // Waits for item to be removed from table + getRow(flow_id).should('not.exist'); + } + + createDirectionalFlow(flow_id: string, source_zones: string[], dest_zones: string[]) { + cy.get('cd-rgw-multisite-sync-policy-details').should('exist'); + this.getTab('Flow').should('exist'); + this.getTab('Flow').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 cd-table') + .eq(1) + .find('.table-actions button') + .first() + .click(); + cy.get('cd-rgw-multisite-sync-flow-modal').should('exist'); + cy.wait(WAIT_TIMER); + // Enter in flow_id + cy.get('#flow_id').type(flow_id); + // Select source zone + cy.get('a[data-testid=select-menu-edit]').first().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-flow-modal').click(); + + // Select destination zone + cy.get('a[data-testid=select-menu-edit]').eq(1).click(); + for (const dest_zone of dest_zones) { + cy.get('.popover-body').find('input[type="text"]').type(`${dest_zone}{enter}`); + } + cy.get('button.tc_submitButton').click(); + + cy.get('cd-rgw-multisite-sync-policy-details cd-table') + .eq(1) + .find('[aria-label=search]') + .first() + .clear({ force: true }) + .type(dest_zones[0]); + cy.get('cd-rgw-multisite-sync-policy-details cd-table') + .eq(1) + .find('.datatable-body-cell-label') + .should('contain', dest_zones[0]); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-selector.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-selector.ts new file mode 100644 index 0000000000000..011aa064d128e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite-zone-selector.ts @@ -0,0 +1,33 @@ +import { Validators } from '@angular/forms'; +import { SelectMessages } from '~/app/shared/components/select/select-messages.model'; +import { SelectOption } from '~/app/shared/components/select/select-option.model'; + +interface Zone { + selected: string[]; + available: SelectOption[]; + validators: any[]; + messages: SelectMessages; +} + +export class ZoneData { + data: Zone; + customBadges: boolean; + + constructor(customBadges: boolean = false, filterMsg: string) { + this.customBadges = customBadges; + this.data = { + selected: [], + available: [], + validators: [Validators.pattern('[A-Za-z0-9_-]+|\\*'), Validators.maxLength(50)], + messages: new SelectMessages({ + empty: $localize`No zones added`, + customValidations: { + pattern: $localize`Allowed characters '-_a-zA-Z0-9|*'`, + maxlength: $localize`Maximum length is 50 characters` + }, + filter: $localize`${filterMsg}`, + add: $localize`Add zone` + }) + }; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts index f2fc381e806f1..bc2f8eafe5b45 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/models/rgw-multisite.ts @@ -56,3 +56,8 @@ export enum RgwMultisiteSyncPolicyStatus { FORBIDDEN = 'forbidden', ALLOWED = 'allowed' } + +export enum FlowType { + directional = 'directional', + symmetrical = 'symmetrical' +} 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 new file mode 100644 index 0000000000000..b6f8e3ec4e5af --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.html @@ -0,0 +1,121 @@ + + {{ action | titlecase }} {{ groupType | upperFirst }} Flow + + +
+ + +
+
+
+ + + + + + + {{name?.split('_')}} selection is required! + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.scss new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.spec.ts new file mode 100644 index 0000000000000..fb864bac4570c --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.spec.ts @@ -0,0 +1,40 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RgwMultisiteSyncFlowModalComponent } from './rgw-multisite-sync-flow-modal.component'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ToastrModule } from 'ngx-toastr'; +import { PipesModule } from '~/app/shared/pipes/pipes.module'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; + +enum FlowType { + symmetrical = 'symmetrical', + directional = 'directional' +} +describe('RgwMultisiteSyncFlowModalComponent', () => { + let component: RgwMultisiteSyncFlowModalComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RgwMultisiteSyncFlowModalComponent], + imports: [ + HttpClientTestingModule, + ToastrModule.forRoot(), + PipesModule, + ReactiveFormsModule, + CommonModule + ], + providers: [NgbActiveModal] + }).compileComponents(); + + fixture = TestBed.createComponent(RgwMultisiteSyncFlowModalComponent); + component = fixture.componentInstance; + component.groupType = FlowType.symmetrical; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.ts new file mode 100644 index 0000000000000..95db659cbe363 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-flow-modal/rgw-multisite-sync-flow-modal.component.ts @@ -0,0 +1,179 @@ +import { Component, OnInit } from '@angular/core'; +import { UntypedFormControl, Validators } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service'; +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { NotificationService } from '~/app/shared/services/notification.service'; +import { catchError, switchMap } from 'rxjs/operators'; +import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service'; +import { RgwDaemon } from '../models/rgw-daemon'; +import { FlowType, RgwZonegroup } from '../models/rgw-multisite'; +import { of } from 'rxjs'; +import { SelectOption } from '~/app/shared/components/select/select-option.model'; +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 { ZoneData } from '../models/rgw-multisite-zone-selector'; + +@Component({ + selector: 'cd-rgw-multisite-sync-flow-modal', + templateUrl: './rgw-multisite-sync-flow-modal.component.html', + styleUrls: ['./rgw-multisite-sync-flow-modal.component.scss'] +}) +export class RgwMultisiteSyncFlowModalComponent implements OnInit { + action: string; + editing: boolean = false; + groupType: FlowType; + groupExpandedRow: any; + flowSelectedRow: any; + syncPolicyDirectionalFlowForm: CdFormGroup; + syncPolicySymmetricalFlowForm: CdFormGroup; + syncPolicyPipeForm: CdFormGroup; + currentFormGroupContext: CdFormGroup; + flowType = FlowType; + icons = Icons; + zones = new ZoneData(false, 'Filter Zones'); + sourceZones = new ZoneData(false, 'Filter Zones'); + destinationZones = new ZoneData(true, 'Filter or Add Zones'); + + constructor( + public activeModal: NgbActiveModal, + public actionLabels: ActionLabelsI18n, + public notificationService: NotificationService, + private rgwDaemonService: RgwDaemonService, + private rgwZonegroupService: RgwZonegroupService, + private rgwMultisiteService: RgwMultisiteService + ) {} + + ngOnInit(): void { + if (this.action === 'edit') { + this.editing = true; + } + if (this.groupType === FlowType.symmetrical) { + this.createSymmetricalFlowForm(); + this.currentFormGroupContext = _.cloneDeep(this.syncPolicySymmetricalFlowForm); + } else if (this.groupType === FlowType.directional) { + this.createDirectionalFlowForm(); + this.currentFormGroupContext = _.cloneDeep(this.syncPolicyDirectionalFlowForm); + } + + if (this.editing) { + this.currentFormGroupContext.patchValue({ + flow_id: this.flowSelectedRow.id, + bucket_name: this.groupExpandedRow.bucket || '' + }); + } + + 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) { + const zones: any = []; + zonegroupData.zones.forEach((zone: any) => { + zones.push(new SelectOption(false, zone.name, '')); + }); + this.zones.data.available = [...zones]; + this.sourceZones.data.available = [...zones]; + if (this.editing) { + if (this.groupType === FlowType.symmetrical) { + this.zones.data.selected = this.flowSelectedRow.zones; + } else { + this.destinationZones.data.selected = [this.flowSelectedRow.dest_zone]; + this.sourceZones.data.selected = [this.flowSelectedRow.source_zone]; + } + this.zoneSelection(); + } + } + }); + } + + createSymmetricalFlowForm() { + this.syncPolicySymmetricalFlowForm = new CdFormGroup({ + ...this.commonFormControls(FlowType.symmetrical), + zones: new UntypedFormControl([], { + validators: [Validators.required] + }) + }); + } + + createDirectionalFlowForm() { + this.syncPolicyDirectionalFlowForm = new CdFormGroup({ + ...this.commonFormControls(FlowType.directional), + source_zone: new UntypedFormControl('', { + validators: [Validators.required] + }), + destination_zone: new UntypedFormControl('', { + validators: [Validators.required] + }) + }); + } + + commonFormControls(flowType: FlowType) { + return { + bucket_name: new UntypedFormControl(this.groupExpandedRow?.bucket), + group_id: new UntypedFormControl(this.groupExpandedRow?.groupName, { + validators: [Validators.required] + }), + flow_id: new UntypedFormControl('', { + validators: [Validators.required] + }), + flow_type: new UntypedFormControl(flowType, { + validators: [Validators.required] + }) + }; + } + + zoneSelection() { + if (this.groupType === FlowType.symmetrical) { + this.currentFormGroupContext.patchValue({ + zones: this.zones.data.selected + }); + } else { + this.currentFormGroupContext.patchValue({ + source_zone: this.sourceZones.data.selected, + destination_zone: this.destinationZones.data.selected + }); + } + } + + submit() { + if (this.currentFormGroupContext.invalid) { + return; + } + // Ensure that no validation is pending + if (this.currentFormGroupContext.pending) { + this.currentFormGroupContext.setErrors({ cdSubmitButton: true }); + return; + } + this.rgwMultisiteService.createEditSyncFlow(this.currentFormGroupContext.value).subscribe( + () => { + 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(); + } + ); + } +} 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 new file mode 100644 index 0000000000000..5fabf9d6bcb48 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.html @@ -0,0 +1,89 @@ + + + +
+
+ + + + Are you sure you want to delete these Flow? + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.scss new file mode 100644 index 0000000000000..e1fec97cc92cf --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.scss @@ -0,0 +1,3 @@ +::ng-deep datatable-scroller { + width: 100% !important; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.spec.ts new file mode 100644 index 0000000000000..ae3ab137bfaaa --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.spec.ts @@ -0,0 +1,26 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RgwMultisiteSyncPolicyDetailsComponent } from './rgw-multisite-sync-policy-details.component'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ToastrModule } from 'ngx-toastr'; +import { PipesModule } from '~/app/shared/pipes/pipes.module'; + +describe('RgwMultisiteSyncPolicyDetailsComponent', () => { + let component: RgwMultisiteSyncPolicyDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [RgwMultisiteSyncPolicyDetailsComponent], + imports: [HttpClientTestingModule, ToastrModule.forRoot(), PipesModule] + }).compileComponents(); + + fixture = TestBed.createComponent(RgwMultisiteSyncPolicyDetailsComponent); + 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-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 new file mode 100644 index 0000000000000..14cc7a7f02fae --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.ts @@ -0,0 +1,228 @@ +import { Component, Input, OnChanges, SimpleChanges, TemplateRef, ViewChild } from '@angular/core'; +import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; +import { Icons } from '~/app/shared/enum/icons.enum'; +import { CdTableAction } from '~/app/shared/models/cd-table-action'; +import { CdTableColumn } from '~/app/shared/models/cd-table-column'; +import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; +import { Permission } from '~/app/shared/models/permissions'; +import { ModalService } from '~/app/shared/services/modal.service'; +import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service'; +import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; +import { FinishedTask } from '~/app/shared/models/finished-task'; +import { Observable, Subscriber, forkJoin as observableForkJoin } from 'rxjs'; +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'; + +@Component({ + selector: 'cd-rgw-multisite-sync-policy-details', + templateUrl: './rgw-multisite-sync-policy-details.component.html', + styleUrls: ['./rgw-multisite-sync-policy-details.component.scss'] +}) +export class RgwMultisiteSyncPolicyDetailsComponent implements OnChanges { + @Input() + expandedRow: any; + @Input() + permission: Permission; + + @ViewChild(TableComponent) + table: TableComponent; + @ViewChild('deleteTpl', { static: true }) + deleteTpl: TemplateRef; + + flowType = FlowType; + modalRef: NgbModalRef; + symmetricalFlowData: any = []; + directionalFlowData: any = []; + symmetricalFlowCols: CdTableColumn[]; + directionalFlowCols: CdTableColumn[]; + symFlowTableActions: CdTableAction[]; + dirFlowTableActions: CdTableAction[]; + symFlowSelection = new CdTableSelection(); + dirFlowSelection = new CdTableSelection(); + + constructor( + private actionLabels: ActionLabelsI18n, + private modalService: ModalService, + private rgwMultisiteService: RgwMultisiteService, + private taskWrapper: TaskWrapperService + ) { + this.symmetricalFlowCols = [ + { + name: 'Name', + prop: 'id', + flexGrow: 1 + }, + { + name: 'Zones', + prop: 'zones', + flexGrow: 1 + } + ]; + this.directionalFlowCols = [ + { + name: 'Source Zone', + prop: 'source_zone', + flexGrow: 1 + }, + { + name: 'Destination Zone', + prop: 'dest_zone', + flexGrow: 1 + } + ]; + const symAddAction: CdTableAction = { + permission: 'create', + icon: Icons.add, + name: this.actionLabels.CREATE, + click: () => this.openModal(FlowType.symmetrical), + canBePrimary: (selection: CdTableSelection) => !selection.hasSelection + }; + const symEditAction: CdTableAction = { + permission: 'update', + icon: Icons.edit, + name: this.actionLabels.EDIT, + click: () => this.openModal(FlowType.symmetrical, true) + }; + const symDeleteAction: CdTableAction = { + permission: 'delete', + icon: Icons.destroy, + disable: () => !this.symFlowSelection.hasSelection, + name: this.actionLabels.DELETE, + click: () => this.deleteFlow(FlowType.symmetrical), + canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection + }; + this.symFlowTableActions = [symAddAction, symEditAction, symDeleteAction]; + const dirAddAction: CdTableAction = { + permission: 'create', + icon: Icons.add, + name: this.actionLabels.CREATE, + click: () => this.openModal(FlowType.directional), + canBePrimary: (selection: CdTableSelection) => !selection.hasSelection + }; + const dirEditAction: CdTableAction = { + permission: 'update', + icon: Icons.edit, + name: this.actionLabels.EDIT, + click: () => this.openModal(FlowType.directional, true), + disable: () => true // TODO: disabling 'edit' as we are not getting flow ID from backend which is needed for edit + }; + const dirDeleteAction: CdTableAction = { + permission: 'delete', + icon: Icons.destroy, + disable: () => true, // TODO: disabling 'delete' as we are not getting flow ID from backend which is needed for deletion + name: this.actionLabels.DELETE, + click: () => this.deleteFlow(FlowType.directional), + canBePrimary: (selection: CdTableSelection) => selection.hasMultiSelection + }; + this.dirFlowTableActions = [dirAddAction, dirEditAction, dirDeleteAction]; + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.expandedRow.currentValue && changes.expandedRow.currentValue.groupName) { + this.symmetricalFlowData = []; + this.directionalFlowData = []; + this.loadFlowData(); + } + } + + loadFlowData(context?: any) { + if (this.expandedRow) { + this.rgwMultisiteService + .getSyncPolicyGroup(this.expandedRow.groupName, this.expandedRow.bucket) + .subscribe( + (policy: any) => { + this.symmetricalFlowData = policy.data_flow[FlowType.symmetrical] || []; + this.directionalFlowData = policy.data_flow[FlowType.directional] || []; + }, + () => { + if (context) { + context.error(); + } + } + ); + } + } + + updateSelection(selection: any, type: FlowType) { + if (type === FlowType.directional) { + this.dirFlowSelection = selection; + } else { + this.symFlowSelection = selection; + } + } + + async openModal(flowType: FlowType, edit = false) { + const action = edit ? 'edit' : 'create'; + const initialState = { + groupType: flowType, + groupExpandedRow: this.expandedRow, + flowSelectedRow: + flowType === FlowType.symmetrical + ? this.symFlowSelection.first() + : this.dirFlowSelection.first(), + action: action + }; + + this.modalRef = this.modalService.show(RgwMultisiteSyncFlowModalComponent, initialState, { + size: 'lg' + }); + + try { + const res = await this.modalRef.result; + if (res === 'success') { + this.loadFlowData(); + } + } catch (err) {} + } + + deleteFlow(flowType: FlowType) { + let selection = this.symFlowSelection; + if (flowType === FlowType.directional) { + selection = this.dirFlowSelection; + } + const flowIds = selection.selected.map((flow: any) => flow.id); + this.modalService.show(CriticalConfirmationModalComponent, { + itemDescription: selection.hasSingleSelection ? $localize`Flow` : $localize`Flows`, + itemNames: flowIds, + bodyTemplate: this.deleteTpl, + submitActionObservable: () => { + return new Observable((observer: Subscriber) => { + this.taskWrapper + .wrapTaskAroundCall({ + task: new FinishedTask('rgw/multisite/sync-flow/delete', { + flow_ids: flowIds + }), + call: observableForkJoin( + selection.selected.map((flow: any) => { + return this.rgwMultisiteService.removeSyncFlow( + flow.id, + flowType, + 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-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 index 8909f44c32b39..2dd4e239cf57f 100644 --- 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 @@ -2,7 +2,8 @@ Multisite Sync Policy Multisite bucket-granularity sync policy provides fine grained control of data movement between - buckets in different zones. + buckets in different zones. Leveraging the bucket-granularity sync policy is possible for buckets to diverge, + and a bucket can pull data from other buckets (ones that don’t share its name or its ID) in different zone.
@@ -30,6 +28,11 @@ [tableActions]="tableActions">
+ +
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 index 2ed1ec0f7e2c2..422e796671b38 100644 --- 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 @@ -2,6 +2,7 @@ import { TitleCasePipe } from '@angular/common'; import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { forkJoin as observableForkJoin, Observable, Subscriber } from 'rxjs'; import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service'; +import { ListWithDetails } from '~/app/shared/classes/list-with-details.class'; import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component'; import { ActionLabelsI18n } from '~/app/shared/constants/app.constants'; import { TableComponent } from '~/app/shared/datatable/table/table.component'; @@ -26,7 +27,7 @@ const BASE_URL = 'rgw/multisite/sync-policy'; styleUrls: ['./rgw-multisite-sync-policy.component.scss'], providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }] }) -export class RgwMultisiteSyncPolicyComponent implements OnInit { +export class RgwMultisiteSyncPolicyComponent extends ListWithDetails implements OnInit { @ViewChild(TableComponent, { static: true }) table: TableComponent; @ViewChild('deleteTpl', { static: true }) @@ -46,7 +47,9 @@ export class RgwMultisiteSyncPolicyComponent implements OnInit { private authStorageService: AuthStorageService, private modalService: ModalService, private taskWrapper: TaskWrapperService - ) {} + ) { + super(); + } ngOnInit(): void { this.permission = this.authStorageService.getPermissions().rgw; 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 9767230ba1b40..66a7c5d2db4f8 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 @@ -59,6 +59,8 @@ import { RgwMultisiteSyncPolicyFormComponent } from './rgw-multisite-sync-policy import { RgwConfigurationPageComponent } from './rgw-configuration-page/rgw-configuration-page.component'; import { RgwConfigDetailsComponent } from './rgw-config-details/rgw-config-details.component'; import { RgwMultisiteWizardComponent } from './rgw-multisite-wizard/rgw-multisite-wizard.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'; @NgModule({ imports: [ @@ -122,7 +124,9 @@ import { RgwMultisiteWizardComponent } from './rgw-multisite-wizard/rgw-multisit RgwMultisiteSyncPolicyFormComponent, RgwConfigDetailsComponent, RgwConfigurationPageComponent, - RgwMultisiteWizardComponent + RgwMultisiteWizardComponent, + RgwMultisiteSyncPolicyDetailsComponent, + RgwMultisiteSyncFlowModalComponent ], 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 424eb21e41b2a..cdc85b25b9fb5 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 @@ -89,4 +89,84 @@ describe('RgwMultisiteService', () => { expect(req.request.method).toBe('GET'); req.flush(mockSyncPolicyData[1]); }); + + it('should create Symmetrical Sync flow', () => { + const payload = { + group_id: 'test', + bucket_name: 'test', + flow_type: 'symmetrical', + flow_id: 'new-flow', + zones: ['zone1-zg1-realm1'] + }; + 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 create Directional Sync flow', () => { + const payload = { + group_id: 'test', + bucket_name: 'test', + flow_type: 'directional', + flow_id: 'new-flow', + source_zone: ['zone1-zg1-realm1'], + destination_zone: ['zone1-zg2-realm2'] + }; + 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 edit Symmetrical Sync flow', () => { + const payload = { + group_id: 'test', + bucket_name: 'test', + flow_type: 'symmetrical', + flow_id: 'new-flow', + zones: ['zone1-zg1-realm1', 'zone2-zg1-realm1'] + }; + 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 edit Directional Sync flow', () => { + const payload = { + group_id: 'test', + bucket_name: 'test', + flow_type: 'directional', + flow_id: 'new-flow', + source_zone: ['zone1-zg1-realm1'], + destination_zone: ['zone1-zg2-realm2', 'zone2-zg2-realm2'] + }; + 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 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` + ); + expect(req.request.method).toBe('DELETE'); + req.flush(null); + }); + + 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` + ); + 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 e77e6afab1f92..e9d4f398707e6 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 @@ -95,4 +95,21 @@ export class RgwMultisiteService { return this.http.post(`${this.uiUrl}/multisite-replications`, null, { params: params }); } + + createEditSyncFlow(payload: any) { + return this.http.put(`${this.url}/sync-flow`, payload); + } + + 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 } + ); + } } 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 71621072783c1..0e966a9474b7b 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 @@ -334,6 +334,16 @@ export class TaskMessageService { }`; } ), + 'rgw/multisite/sync-flow/delete': this.newTaskMessage( + this.commonOperations.delete, + (metadata) => { + return $localize`${ + metadata.flow_ids.length > 1 + ? 'selected Flow Names' + : `Flow Name '${metadata.flow_ids[0]}'` + }`; + } + ), // iSCSI target tasks 'iscsi/target/create': this.newTaskMessage(this.commonOperations.create, (metadata) => this.iscsiTarget(metadata) diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index ecf305d77acee..98c6c562f0985 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -11168,7 +11168,6 @@ paths: default: '' type: string destination_zone: - default: '' type: string flow_id: type: string @@ -11177,7 +11176,6 @@ paths: group_id: type: string source_zone: - default: '' type: string zones: type: string diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py index 2fb30b67f43c1..4a9b9b66a8bf6 100644 --- a/src/pybind/mgr/dashboard/services/rgw_client.py +++ b/src/pybind/mgr/dashboard/services/rgw_client.py @@ -2072,12 +2072,16 @@ class RgwMultisite: def create_sync_flow(self, group_id: str, flow_id: str, flow_type: str, zones: Optional[List[str]] = None, bucket_name: str = '', - source_zone: str = '', destination_zone: str = ''): + source_zone: Optional[List[str]] = None, + destination_zone: Optional[List[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 SyncFlowTypes[flow_type].value == 'directional': - rgw_sync_policy_cmd += ['--source-zone', source_zone, '--dest-zone', destination_zone] + if source_zone is not None: + rgw_sync_policy_cmd += ['--source-zone', ','.join(source_zone)] + if destination_zone is not None: + rgw_sync_policy_cmd += ['--dest-zone', ','.join(destination_zone)] else: if zones: rgw_sync_policy_cmd += ['--zones', ','.join(zones)] -- 2.39.5