From d6c657b7e6bb85f6d246b389c54941b777364f49 Mon Sep 17 00:00:00 2001 From: Aashish Sharma Date: Wed, 23 Apr 2025 14:53:24 +0530 Subject: [PATCH] mgr/dashboard: Enable rgw module automatically in the primary and secondary cluster if not enabled during multi-site automation 1. Enable rgw module automatically in the primary and secondary cluster if not enabled during multi-site automation 2. Improve progress bar descriptions and add sub-descriptions for steps Fixes: https://tracker.ceph.com/issues/71033 Signed-off-by: Aashish Sharma --- src/pybind/mgr/dashboard/controllers/rgw.py | 14 +- .../mgr-module-list.component.spec.ts | 4 + .../rgw-multisite-details.component.html | 2 +- .../rgw-multisite-details.component.ts | 27 +-- ...site-sync-policy-details.component.spec.ts | 9 +- ...ltisite-sync-policy-form.component.spec.ts | 2 + .../rgw-multisite-wizard.component.html | 16 +- .../rgw-multisite-wizard.component.ts | 158 +++++++++++------- .../rgw-overview-dashboard.component.spec.ts | 5 +- .../app/shared/api/mgr-module.service.spec.ts | 4 + .../src/app/shared/api/mgr-module.service.ts | 123 +++++++------- .../shared/api/rgw-multisite.service.spec.ts | 10 +- .../app/shared/api/rgw-multisite.service.ts | 21 ++- .../src/app/shared/api/rgw-realm.service.ts | 10 +- .../progress/progress.component.html | 5 + .../components/progress/progress.component.ts | 1 + src/pybind/mgr/dashboard/openapi.yaml | 7 +- .../mgr/dashboard/services/rgw_client.py | 134 ++++++++++++++- 18 files changed, 396 insertions(+), 156 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index 757a8b89fcbaa..e4bcf3652d61b 100755 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -1200,10 +1200,16 @@ class RgwRealm(RESTController): @allow_empty_body # pylint: disable=W0613 - def list(self): - multisite_instance = RgwMultisite() - result = multisite_instance.list_realms() - return result + def list(self, replicable: Optional[bool] = None): + if replicable: + try: + multisite_automation_instance = RgwMultisiteAutomation() + return multisite_automation_instance.get_replicable_realms_list() + except NoRgwDaemonsException as e: + raise DashboardException(e, http_status_code=404, component='rgw') + else: + multisite_instance = RgwMultisite() + return multisite_instance.list_realms() @allow_empty_body # pylint: disable=W0613 diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.spec.ts index f7c65a730b5c2..0ce36eba01afa 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.spec.ts @@ -14,6 +14,7 @@ import { SharedModule } from '~/app/shared/shared.module'; import { configureTestBed, PermissionHelper } from '~/testing/unit-test-helper'; import { MgrModuleDetailsComponent } from '../mgr-module-details/mgr-module-details.component'; import { MgrModuleListComponent } from './mgr-module-list.component'; +import { SummaryService } from '~/app/shared/services/summary.service'; describe('MgrModuleListComponent', () => { let component: MgrModuleListComponent; @@ -37,6 +38,8 @@ describe('MgrModuleListComponent', () => { fixture = TestBed.createComponent(MgrModuleListComponent); component = fixture.componentInstance; mgrModuleService = TestBed.inject(MgrModuleService); + const summaryService = TestBed.inject(SummaryService); + spyOn(summaryService, 'startPolling'); }); it('should create', () => { @@ -143,6 +146,7 @@ describe('MgrModuleListComponent', () => { component.updateModuleState(); tick(mgrModuleService.REFRESH_INTERVAL); tick(mgrModuleService.REFRESH_INTERVAL); + tick(mgrModuleService.REFRESH_INTERVAL); expect(mgrModuleService.enable).toHaveBeenCalledWith('foo'); expect(mgrModuleService.list).toHaveBeenCalledTimes(2); expect(component.table.refreshBtn).toHaveBeenCalled(); 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 3f9794e26261a..b47d9230fb69b 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 @@ -8,7 +8,7 @@ class="align-items-center" actionName="Enable" (action)="enableRgwModule()"> - In order to access the import/export/Setup Multi-site Replication feature, the rgw module must be enabled. + In order to access the import/export feature, the rgw module must be enabled. { + this.startPollingMultisiteInfo(); + this.getRgwModuleStatus(); + }); + // Only get the module status if you can read from configOpt + if (this.permissions.configOpt.read) this.getRgwModuleStatus(); + } + + startPollingMultisiteInfo(): void { const observables = [ this.rgwRealmService.getAllRealmsInfo(), this.rgwZonegroupService.getAllZonegroupsInfo(), this.rgwZoneService.getAllZonesInfo() ]; + + if (this.sub) { + this.sub.unsubscribe(); + } + this.sub = this.timerService .get(() => forkJoin(observables), this.timerServiceVariable.TIMER_SERVICE_PERIOD * 2) .subscribe( @@ -305,9 +318,6 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { }, (_error) => {} ); - - // Only get the module status if you can read from configOpt - if (this.permissions.configOpt.read) this.getRgwModuleStatus(); } ngOnDestroy() { @@ -315,11 +325,8 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit { } private getRgwModuleStatus() { - this.mgrModuleService.list().subscribe((moduleData: MgrModuleInfo[]) => { - this.rgwModuleData = moduleData.filter((module: MgrModuleInfo) => module.name === RGW); - if (this.rgwModuleData.length > 0) { - this.rgwModuleStatus = this.rgwModuleData[0].enabled; - } + this.rgwMultisiteService.getRgwModuleStatus().subscribe((status: boolean) => { + this.rgwModuleStatus = status; }); } 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 index 82d275971d21e..94e8078c1c257 100644 --- 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 @@ -5,6 +5,7 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ToastrModule } from 'ngx-toastr'; import { PipesModule } from '~/app/shared/pipes/pipes.module'; import { ModalModule } from 'carbon-components-angular'; +import { SharedModule } from '~/app/shared/shared.module'; describe('RgwMultisiteSyncPolicyDetailsComponent', () => { let component: RgwMultisiteSyncPolicyDetailsComponent; @@ -13,7 +14,13 @@ describe('RgwMultisiteSyncPolicyDetailsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [RgwMultisiteSyncPolicyDetailsComponent], - imports: [HttpClientTestingModule, ToastrModule.forRoot(), PipesModule, ModalModule] + imports: [ + HttpClientTestingModule, + ToastrModule.forRoot(), + PipesModule, + ModalModule, + SharedModule + ] }).compileComponents(); fixture = TestBed.createComponent(RgwMultisiteSyncPolicyDetailsComponent); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component.spec.ts index c09e7392a0b63..d3c2a3227e131 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component.spec.ts @@ -7,6 +7,7 @@ import { PipesModule } from '~/app/shared/pipes/pipes.module'; import { ComponentsModule } from '~/app/shared/components/components.module'; import { RouterTestingModule } from '@angular/router/testing'; import { CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA } from '@angular/core'; +import { SharedModule } from '~/app/shared/shared.module'; describe('RgwMultisiteSyncPolicyFormComponent', () => { let component: RgwMultisiteSyncPolicyFormComponent; @@ -21,6 +22,7 @@ describe('RgwMultisiteSyncPolicyFormComponent', () => { ToastrModule.forRoot(), PipesModule, ComponentsModule, + SharedModule, RouterTestingModule ], schemas: [NO_ERRORS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA], diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.html index 3658ebf5451ae..199b9d854d2c1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.html @@ -73,9 +73,9 @@ @@ -335,7 +335,8 @@ + [description]="executingTask?.name?.replace('progress/Multisite-Setup:', '')?.split('||')[0]?.trim()" + [subDescription]="executingTask?.name?.replace('progress/Multisite-Setup:', '')?.split('||')[1]?.trim()"> @@ -380,6 +381,13 @@ + + + During the automation process, the RGW module will be enabled on both the source and target clusters, if it is not already enabled. + This action may cause a temporary downtime (5-10 seconds) on each cluster. + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.ts index 4872ffb3dbda1..34773cddbf7b4 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, NgZone, OnInit } from '@angular/core'; import { Location } from '@angular/common'; import { UntypedFormControl, Validators } from '@angular/forms'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; @@ -29,6 +29,7 @@ import { } from './multisite-wizard-steps.enum'; import { RgwRealmService } from '~/app/shared/api/rgw-realm.service'; import { MultiCluster, MultiClusterConfig } from '~/app/shared/models/multi-cluster'; +import { MgrModuleService } from '~/app/shared/api/mgr-module.service'; interface DaemonStats { rgw_metadata?: { @@ -80,7 +81,7 @@ export class RgwMultisiteWizardComponent extends BaseModal implements OnInit { setupCompleted = false; showConfigType = false; realmList: string[] = []; - realmsInfo: { realm: string; token: string }[]; + rgwModuleStatus: boolean; constructor( private wizardStepsService: WizardStepsService, @@ -94,7 +95,9 @@ export class RgwMultisiteWizardComponent extends BaseModal implements OnInit { private route: ActivatedRoute, private summaryService: SummaryService, private location: Location, - private cdr: ChangeDetectorRef + private cdr: ChangeDetectorRef, + private mgrModuleService: MgrModuleService, + private zone: NgZone ) { super(); this.pageURL = 'rgw/multisite/configuration'; @@ -138,24 +141,30 @@ export class RgwMultisiteWizardComponent extends BaseModal implements OnInit { }); this.summaryService.subscribe((summary) => { - this.executingTask = summary.executing_tasks.find((task) => - task.name.includes('progress/Multisite-Setup') - ); + this.zone.run(() => { + this.executingTask = summary.executing_tasks.find((task) => + task.name.includes('progress/Multisite-Setup') + ); + this.cdr.detectChanges(); + }); }); this.stepTitles.forEach((step) => { this.stepsToSkip[step.label] = false; }); - this.rgwRealmService.getRealmTokens().subscribe((data: { realm: string; token: string }[]) => { - const base64Matcher = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/; - this.realmsInfo = data.filter((realmInfo) => base64Matcher.test(realmInfo.token)); - this.showConfigType = this.realmsInfo.length > 0; + this.rgwRealmService.list().subscribe((realms: string[]) => { + this.realmList = realms; + this.showConfigType = this.realmList.length > 0; if (this.showConfigType) { - this.multisiteSetupForm.get('selectedRealm')?.setValue(this.realmsInfo[0].realm); + this.multisiteSetupForm.get('selectedRealm')?.setValue(this.realmList[0]); this.cdr.detectChanges(); } }); + + this.rgwMultisiteService.getRgwModuleStatus().subscribe((status: boolean) => { + this.rgwModuleStatus = status; + }); } private loadRGWEndpoints(): void { @@ -271,61 +280,86 @@ export class RgwMultisiteWizardComponent extends BaseModal implements OnInit { onSubmit() { this.loading = true; - const values = this.multisiteSetupForm.getRawValue(); - const realmName = values['realmName']; - const zonegroupName = values['zonegroupName']; - const zonegroupEndpoints = this.rgwEndpoints.value.join(','); - const zoneName = values['zoneName']; - const zoneEndpoints = this.rgwEndpoints.value.join(','); - const username = values['username']; - if (!this.isMultiClusterConfigured || this.stepsToSkip['Select Cluster']) { - this.rgwMultisiteService - .setUpMultisiteReplication( - realmName, - zonegroupName, - zonegroupEndpoints, - zoneName, - zoneEndpoints, - username - ) - .subscribe((data: object[]) => { - this.setupCompleted = true; - this.rgwMultisiteService.setRestartGatewayMessage(false); - this.loading = false; - this.realms = data; - this.showSuccessNotification(); - }); - } else { - const cluster = values['cluster']; - const replicationZoneName = values['replicationZoneName']; - let selectedRealmName = ''; - if (this.multisiteSetupForm.get('configType').value === ConfigType.ExistingRealm) { - selectedRealmName = this.multisiteSetupForm.get('selectedRealm').value; - } - this.rgwMultisiteService - .setUpMultisiteReplication( - realmName, - zonegroupName, - zonegroupEndpoints, - zoneName, - zoneEndpoints, - username, - cluster, - replicationZoneName, - this.clusterDetailsArray, - selectedRealmName - ) - .subscribe( - () => { + + const proceedWithSetup = () => { + this.cdr.detectChanges(); + const values = this.multisiteSetupForm.getRawValue(); + const realmName = values['realmName']; + const zonegroupName = values['zonegroupName']; + const zonegroupEndpoints = this.rgwEndpoints.value.join(','); + const zoneName = values['zoneName']; + const zoneEndpoints = this.rgwEndpoints.value.join(','); + const username = values['username']; + + if (!this.isMultiClusterConfigured || this.stepsToSkip['Select Cluster']) { + this.rgwMultisiteService + .setUpMultisiteReplication( + realmName, + zonegroupName, + zonegroupEndpoints, + zoneName, + zoneEndpoints, + username + ) + .subscribe((data: object[]) => { this.setupCompleted = true; this.rgwMultisiteService.setRestartGatewayMessage(false); this.loading = false; + this.realms = data; this.showSuccessNotification(); - }, - () => { - this.multisiteSetupForm.setErrors({ cdSubmitButton: true }); - } - ); + }); + } else { + const cluster = values['cluster']; + const replicationZoneName = values['replicationZoneName']; + let selectedRealmName = ''; + + if (this.multisiteSetupForm.get('configType').value === ConfigType.ExistingRealm) { + selectedRealmName = this.multisiteSetupForm.get('selectedRealm').value; + } + + this.rgwMultisiteService + .setUpMultisiteReplication( + realmName, + zonegroupName, + zonegroupEndpoints, + zoneName, + zoneEndpoints, + username, + cluster, + replicationZoneName, + this.clusterDetailsArray, + selectedRealmName + ) + .subscribe( + () => { + this.setupCompleted = true; + this.rgwMultisiteService.setRestartGatewayMessage(false); + this.loading = false; + this.showSuccessNotification(); + }, + () => { + this.multisiteSetupForm.setErrors({ cdSubmitButton: true }); + } + ); + } + }; + + if (!this.rgwModuleStatus) { + this.mgrModuleService.updateModuleState( + 'rgw', + false, + null, + '', + '', + false, + $localize`RGW module is being enabled. Waiting for the system to reconnect...` + ); + const subscription = this.mgrModuleService.updateCompleted$.subscribe(() => { + subscription.unsubscribe(); + proceedWithSetup(); + }); + } else { + proceedWithSetup(); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts index 079483ef2003c..03077cd47a8b5 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts @@ -12,8 +12,9 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { RgwRealmService } from '~/app/shared/api/rgw-realm.service'; import { RgwZoneService } from '~/app/shared/api/rgw-zone.service'; import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service'; -import { ToastrModule } from 'ngx-toastr'; import { SharedModule } from '~/app/shared/shared.module'; +import { ToastrModule } from 'ngx-toastr'; +import { CommonModule } from '@angular/common'; import { ActivatedRoute } from '@angular/router'; describe('RgwOverviewDashboardComponent', () => { @@ -89,7 +90,7 @@ describe('RgwOverviewDashboardComponent', () => { useValue: { params: { subscribe: (fn: Function) => fn(params) } } } ], - imports: [HttpClientTestingModule, ToastrModule.forRoot(), SharedModule] + imports: [HttpClientTestingModule, SharedModule, ToastrModule.forRoot(), CommonModule] }).compileComponents(); fixture = TestBed.createComponent(RgwOverviewDashboardComponent); component = fixture.componentInstance; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.spec.ts index da9f0cdb05dbf..6479b6cd78cb3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.spec.ts @@ -10,6 +10,7 @@ import { MgrModuleListComponent } from '~/app/ceph/cluster/mgr-modules/mgr-modul import { ToastrModule } from 'ngx-toastr'; import { SharedModule } from '../shared.module'; import { BlockUIService } from 'ng-block-ui'; +import { SummaryService } from '../services/summary.service'; describe('MgrModuleService', () => { let service: MgrModuleService; @@ -88,6 +89,8 @@ describe('MgrModuleService', () => { spyOn(notificationService, 'suspendToasties'); spyOn(blockUIService, 'start'); spyOn(blockUIService, 'stop'); + const summaryService = TestBed.inject(SummaryService); + spyOn(summaryService, 'startPolling'); }); it('should enable module', fakeAsync(() => { @@ -102,6 +105,7 @@ describe('MgrModuleService', () => { service.updateModuleState(selected.name, selected.enabled); tick(service.REFRESH_INTERVAL); tick(service.REFRESH_INTERVAL); + tick(service.REFRESH_INTERVAL); expect(service.enable).toHaveBeenCalledWith('foo'); expect(service.list).toHaveBeenCalledTimes(2); expect(notificationService.suspendToasties).toHaveBeenCalledTimes(2); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.ts index 940a8b5540416..7826523b5d0ae 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.ts @@ -2,18 +2,23 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { BlockUIService } from 'ng-block-ui'; -import { Observable, timer as observableTimer } from 'rxjs'; +import { Observable, Subject, timer } from 'rxjs'; import { NotificationService } from '../services/notification.service'; import { TableComponent } from '../datatable/table/table.component'; import { Router } from '@angular/router'; import { MgrModuleInfo } from '../models/mgr-modules.interface'; import { NotificationType } from '../enum/notification-type.enum'; +import { delay, retryWhen, switchMap, tap } from 'rxjs/operators'; +import { SummaryService } from '../services/summary.service'; + +const GLOBAL = 'global'; @Injectable({ providedIn: 'root' }) export class MgrModuleService { private url = 'api/mgr/module'; + updateCompleted$ = new Subject(); readonly REFRESH_INTERVAL = 2000; @@ -21,7 +26,8 @@ export class MgrModuleService { private blockUI: BlockUIService, private http: HttpClient, private notificationService: NotificationService, - private router: Router + private router: Router, + private summaryService: SummaryService ) {} /** @@ -85,65 +91,64 @@ export class MgrModuleService { table: TableComponent = null, navigateTo: string = '', notificationText?: string, - navigateByUrl?: boolean + navigateByUrl?: boolean, + reconnectingMessage: string = $localize`Reconnecting, please wait ...` ): void { - let $obs; - const fnWaitUntilReconnected = () => { - observableTimer(this.REFRESH_INTERVAL).subscribe(() => { - // Trigger an API request to check if the connection is - // re-established. - this.list().subscribe( - () => { - // Resume showing the notification toasties. - this.notificationService.suspendToasties(false); - // Unblock the whole UI. - this.blockUI.stop('global'); - // Reload the data table content. - if (table) { - table.refreshBtn(); - } - - if (notificationText) { - this.notificationService.show( - NotificationType.success, - $localize`${notificationText}` - ); - } - - if (!navigateTo) return; - - const navigate = () => this.router.navigate([navigateTo]); - - if (navigateByUrl) { - this.router.navigateByUrl('/', { skipLocationChange: true }).then(navigate); - } else { - navigate(); - } - }, - () => { - fnWaitUntilReconnected(); - } - ); - }); - }; - - // Note, the Ceph Mgr is always restarted when a module - // is enabled/disabled. - if (enabled) { - $obs = this.disable(module); - } else { - $obs = this.enable(module); - } - $obs.subscribe( - () => undefined, - () => { - // Suspend showing the notification toasties. + const moduleToggle$ = enabled ? this.disable(module) : this.enable(module); + + moduleToggle$.subscribe({ + next: () => { + // Module toggle succeeded + this.updateCompleted$.next(); + }, + error: () => { + // Module toggle failed, trigger reconnect flow this.notificationService.suspendToasties(true); - // Block the whole UI to prevent user interactions until - // the connection to the backend is reestablished - this.blockUI.start('global', $localize`Reconnecting, please wait ...`); - fnWaitUntilReconnected(); + this.blockUI.start(GLOBAL, reconnectingMessage); + + timer(this.REFRESH_INTERVAL) + .pipe( + switchMap(() => this.list()), + retryWhen((errors) => + errors.pipe( + tap(() => { + // Keep retrying until list() succeeds + }), + delay(this.REFRESH_INTERVAL) + ) + ) + ) + .subscribe({ + next: () => { + // Reconnection successful + this.notificationService.suspendToasties(false); + this.blockUI.stop(GLOBAL); + + if (table) { + table.refreshBtn(); + } + + if (notificationText) { + this.notificationService.show( + NotificationType.success, + $localize`${notificationText}` + ); + } + + if (navigateTo) { + const navigate = () => this.router.navigate([navigateTo]); + if (navigateByUrl) { + this.router.navigateByUrl('/', { skipLocationChange: true }).then(navigate); + } else { + navigate(); + } + } + + this.updateCompleted$.next(); + this.summaryService.startPolling(); + } + }); } - ); + }); } } 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 01e4ccb9945a9..27b1e5fe54579 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 @@ -2,6 +2,9 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/ import { TestBed } from '@angular/core/testing'; import { configureTestBed } from '~/testing/unit-test-helper'; import { RgwMultisiteService } from './rgw-multisite.service'; +import { BlockUIModule } from 'ng-block-ui'; +import { ToastrModule } from 'ngx-toastr'; +import { SharedModule } from '../shared.module'; const mockSyncPolicyData: any = [ { @@ -25,7 +28,12 @@ describe('RgwMultisiteService', () => { configureTestBed({ providers: [RgwMultisiteService], - imports: [HttpClientTestingModule] + imports: [ + HttpClientTestingModule, + BlockUIModule.forRoot(), + ToastrModule.forRoot(), + SharedModule + ] }); beforeEach(() => { 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 4063c619e8822..808362b90fb6b 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 @@ -2,7 +2,11 @@ import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { RgwRealm, RgwZone, RgwZonegroup } from '~/app/ceph/rgw/models/rgw-multisite'; import { RgwDaemonService } from './rgw-daemon.service'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { MgrModuleInfo } from '../models/mgr-modules.interface'; +import { RGW } from '~/app/ceph/rgw/utils/constants'; +import { MgrModuleService } from './mgr-module.service'; @Injectable({ providedIn: 'root' @@ -14,7 +18,11 @@ export class RgwMultisiteService { private restartGatewayMessageSource = new BehaviorSubject(null); restartGatewayMessage$ = this.restartGatewayMessageSource.asObservable(); - constructor(private http: HttpClient, public rgwDaemonService: RgwDaemonService) {} + constructor( + private http: HttpClient, + public rgwDaemonService: RgwDaemonService, + private mgrModuleService: MgrModuleService + ) {} migrate(realm: RgwRealm, zonegroup: RgwZonegroup, zone: RgwZone, username: string) { return this.rgwDaemonService.request((params: HttpParams) => { @@ -158,4 +166,13 @@ export class RgwMultisiteService { setRestartGatewayMessage(value: boolean): void { this.restartGatewayMessageSource.next(value); } + + getRgwModuleStatus(): Observable { + return this.mgrModuleService.list().pipe( + map((moduleData: MgrModuleInfo[]) => { + const rgwModule = moduleData.find((module) => module.name === RGW); + return !!rgwModule?.enabled; + }) + ); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts index e81731cd52030..777ff061e147f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts @@ -30,8 +30,14 @@ export class RgwRealmService { return this.http.put(`${this.url}/${realm.name}`, requestBody); } - list(): Observable { - return this.http.get(`${this.url}`); + list(replicable?: boolean): Observable { + let params = new HttpParams(); + if (replicable) { + params = params.appendAll({ + replicable: replicable.toString() + }); + } + return this.http.get(`${this.url}`, { params }); } get(realm: RgwRealm): Observable { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/progress/progress.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/progress/progress.component.html index 9d844576d4305..38dcb13a574cd 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/progress/progress.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/progress/progress.component.html @@ -25,6 +25,11 @@ *ngIf="description"> {{ description }} + +

+ {{ subDescription }} +

diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/progress/progress.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/progress/progress.component.ts index b39ebf6b30b34..b9453da5ada96 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/progress/progress.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/progress/progress.component.ts @@ -13,6 +13,7 @@ export class ProgressComponent { @Input() status: string; @Input() description: string; @Input() subLabel: string; + @Input() subDescription: string; @Input() completedItems: string; @Input() actionName: string; @Input() helperText: string; diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 939b2a5fd20ea..d65e2aa91ec48 100755 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -12657,7 +12657,12 @@ paths: - RgwMultisite /api/rgw/realm: get: - parameters: [] + parameters: + - allowEmptyValue: true + in: query + name: replicable + schema: + type: string responses: '200': content: diff --git a/src/pybind/mgr/dashboard/services/rgw_client.py b/src/pybind/mgr/dashboard/services/rgw_client.py index cefad2b045bb7..081c89ba23ae0 100755 --- a/src/pybind/mgr/dashboard/services/rgw_client.py +++ b/src/pybind/mgr/dashboard/services/rgw_client.py @@ -1236,7 +1236,7 @@ class RgwMultisiteAutomation: if not selectedRealmName: self.create_realm_and_zonegroup( - realm_name, zonegroup_name, zone_name, zonegroup_ip_url) + realm_name, zonegroup_name, zone_name, zonegroup_ip_url, username) self.create_zone_and_user(zone_name, zonegroup_name, username, zone_ip_url) self.restart_daemons() @@ -1255,10 +1255,15 @@ class RgwMultisiteAutomation: logger.error("Failed to update endpoints: %s", e) raise - def create_realm_and_zonegroup(self, realm: str, zg: str, zone: str, zg_url: str): + def create_realm_and_zonegroup(self, realm: str, zg: str, zone: str, zg_url: str, + username: str): try: rgw_multisite_instance = RgwMultisite() - self.update_progress(f"Creating realm: {realm}, zonegroup: {zg} and zone: {zone}") + self.update_progress( + f"Initializing multi-site configuration || Creating realm: {realm}, \ + zonegroup: {zg}, and zone: {zone} along \ + with system user: {username}" + ) rgw_multisite_instance.create_realm(realm_name=realm, default=True) rgw_multisite_instance.create_zonegroup(realm_name=realm, zonegroup_name=zg, default=True, master=True, endpoints=zg_url) @@ -1293,7 +1298,10 @@ class RgwMultisiteAutomation: def restart_daemons(self): try: - self.update_progress("Restarting RGW daemons and setting credentials") + self.update_progress( + "Restarting RGW daemons and configuring credentials || Restarts rgw services and \ + applies access and secret keys on the source cluster" + ) RgwServiceManager().restart_rgw_daemons_and_set_credentials() self.progress_done += 1 except Exception as e: @@ -1308,7 +1316,11 @@ class RgwMultisiteAutomation: try: realm_token_info = CephService.get_realm_tokens() if fsid and realm_token_info and rep_zone and details_dict: - self.update_progress(f"Importing realm token to cluster: {fsid}") + self.update_progress( + f"Setting up replication on cluster {fsid} || Enabling RGW module on \ + target cluster (if disabled), importing realm \ + configuration, and establishing the target zone" + ) self.import_realm_token_to_cluster(fsid, realm, zg, realm_token_info, username, rep_zone, details_dict, selectedRealm) else: @@ -1333,6 +1345,8 @@ class RgwMultisiteAutomation: realm_export_token = self._get_realm_export_token(realm_token_info, realm_name) cluster_url, cluster_token = self._get_cluster_details(cluster_fsid, cluster_details) + self._enable_rgw_module(cluster_url=cluster_url, cluster_token=cluster_token) + self._configure_selected_cluster(cluster_url, cluster_token, realm_name, zonegroup_name, replication_zone_name) @@ -1341,8 +1355,11 @@ class RgwMultisiteAutomation: replication_zone_name) self.progress_done += 1 - self.update_progress(f"Checking for user {username} in the selected cluster \ - and setting credentials") + self.update_progress( + f"Verifying system user and completing replication setup on \ + cluster {cluster_fsid} || Ensuring presence of user '{username}' \ + and assigning necessary RGW credentials" + ) self._verify_user_and_daemons(cluster_url, cluster_token, realm_name, replication_zone_name, username) @@ -1354,6 +1371,64 @@ class RgwMultisiteAutomation: self.update_progress("Failed to import realm token to cluster:", 'fail', str(e)) raise + def _enable_rgw_module(self, cluster_url, cluster_token): + # Enable RGW module if not already enabled + mgr_modules_path = 'api/mgr/module' + multi_cluster_instance = MultiCluster() + # pylint: disable=protected-access + mgr_modules_info = multi_cluster_instance._proxy( + method='GET', + base_url=cluster_url, + path=mgr_modules_path, + token=cluster_token + ) + logger.debug("mgr modules info in the selected cluster: %s", mgr_modules_info) + + rgw_module = next((mod for mod in mgr_modules_info if mod["name"] == "rgw"), None) + rgw_module_status = rgw_module and rgw_module.get('enabled', False) + + if not rgw_module_status: + logger.info("RGW module not enabled. Sending request to enable it.") + try: + # pylint: disable=protected-access + multi_cluster_instance._proxy( + method='POST', + base_url=cluster_url, + path='api/mgr/module/rgw/enable', + token=cluster_token + ) + except Exception as e: # pylint: disable=broad-except + logger.warning("RGW enable request failed (likely due to connection reset).\ + Ignoring and retrying later: %s", e) + + max_retries = 10 + delay = 5 + retries = 0 + + while retries < max_retries: + time.sleep(delay) + try: + # pylint: disable=protected-access + mgr_modules_info = multi_cluster_instance._proxy( + method='GET', + base_url=cluster_url, + path=mgr_modules_path, + token=cluster_token + ) + rgw_module = next((mod for mod in mgr_modules_info if mod["name"] == "rgw"), + None) + if rgw_module and rgw_module.get('enabled'): + logger.info("RGW module is now enabled after %d retries.", retries) + break + except Exception as e: # pylint: disable=broad-except + logger.warning("Failed to fetch RGW module status on retry %d: %s", + retries, str(e)) + retries += 1 + else: + logger.error("RGW module failed to enable after %d retries.", max_retries) + raise DashboardException('RGW module failed to enable after maximum retries', + http_status_code=500, component='rgw') + def _get_realm_export_token(self, realm_token_info, realm_name): for realm_token in realm_token_info: if realm_token['realm'] == realm_name: @@ -1479,6 +1554,38 @@ class RgwMultisiteAutomation: logger.info("User %s not found yet, retrying in 5 seconds", username) time.sleep(5) + # For a realm to be replicable it must have a master zone, + # valid endpoints and access/secret keys should be set + def get_replicable_realms_list(self): + replicable_realms = [] + realms_info = RgwMultisite().list_realms() + realm_list = realms_info.get('realms', []) + for realm_name in realm_list: # pylint: disable=R1702 + try: + realm_period = RgwMultisite().get_realm_period(realm_name) + master_zone_name = None + for zg in realm_period['period_map']['zonegroups']: + if not zg.get('is_master'): + continue + for zone in zg.get('zones', []): + if zone.get('id') == zg.get('master_zone'): + if zone.get('endpoints'): + master_zone_name = zone.get('name') + break + if not master_zone_name: + continue + zone_info = RgwMultisite().get_zone(master_zone_name) + system_key = zone_info.get('system_key', {}) + access_key = system_key.get('access_key') + secret_key = system_key.get('secret_key') + if access_key and secret_key: + replicable_realms.append(realm_name) + except Exception as e: # pylint: disable=broad-except + logger.warning("Skipping realm '%s' due to error: %s", realm_name, e) + continue + + return replicable_realms + class RgwRateLimit: def get_global_rateLimit(self): @@ -1660,6 +1767,19 @@ class RgwMultisite: raise DashboardException(error, http_status_code=500, component='rgw') return rgw_realm_list + def get_realm_period(self, realm_name: str): + realm_period = {} + rgw_realm_period_cmd = ['period', 'get', '--rgw-realm', realm_name] + try: + exit_code, out, _ = mgr.send_rgwadmin_command(rgw_realm_period_cmd) + if exit_code > 0: + raise DashboardException('Unable to get realm period', + http_status_code=500, component='rgw') + realm_period = out + except SubprocessError as error: + raise DashboardException(error, http_status_code=500, component='rgw') + return realm_period + def get_realm(self, realm_name: str): realm_info = {} rgw_realm_info_cmd = ['realm', 'get', '--rgw-realm', realm_name] -- 2.39.5