]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Enable rgw module automatically in the primary and secondary cluster... 62929/head
authorAashish Sharma <Aashish.Sharma1@ibm.com>
Wed, 23 Apr 2025 09:23:24 +0000 (14:53 +0530)
committerAashish Sharma <Aashish.Sharma1@ibm.com>
Mon, 30 Jun 2025 05:31:24 +0000 (11:01 +0530)
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 <aasharma@redhat.com>
18 files changed:
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/mgr-modules/mgr-module-list/mgr-module-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-details/rgw-multisite-details.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-details/rgw-multisite-sync-policy-details.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-multisite-wizard/rgw-multisite-wizard.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/mgr-module.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-realm.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/progress/progress.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/progress/progress.component.ts
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/rgw_client.py

index 757a8b89fcbaa0da519b9c4cd3d5f538a78901ad..e4bcf3652d61bbab6617d3ca1fdc31824222eb4c 100755 (executable)
@@ -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
index f7c65a730b5c274f28c6654770c13ea7110bfbf3..0ce36eba01afa107ca478120342e6a4023cc8d2b 100644 (file)
@@ -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();
index 3f9794e26261ab86b6184ffa196ba9e85853707b..b47d9230fb69b90cd891194fd021dba084fa13ad 100644 (file)
@@ -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.
   </cd-alert-panel>
   <cd-alert-panel   *ngIf="restartGatewayMessage"
                     type="warning"
index 6a4c9495151ac847aa497766f2e663c4fbb3213e..27838042e1b24cfcdf6674ec3a9746d468b3e339 100644 (file)
@@ -42,8 +42,6 @@ import { RgwMultisiteWizardComponent } from '../rgw-multisite-wizard/rgw-multisi
 import { RgwMultisiteSyncPolicyComponent } from '../rgw-multisite-sync-policy/rgw-multisite-sync-policy.component';
 import { ModalCdsService } from '~/app/shared/services/modal-cds.service';
 import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
-import { MgrModuleInfo } from '~/app/shared/models/mgr-modules.interface';
-import { RGW } from '../utils/constants';
 
 const BASE_URL = 'rgw/multisite/configuration';
 
@@ -290,11 +288,26 @@ export class RgwMultisiteDetailsComponent implements OnDestroy, OnInit {
       }
     ];
 
+    this.startPollingMultisiteInfo();
+    this.mgrModuleService.updateCompleted$.subscribe(() => {
+      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;
     });
   }
 
index 82d275971d21ece6f21d29472b95162c76b558bb..94e8078c1c257f15b680d4cb728c5a914e2d6efc 100644 (file)
@@ -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);
index c09e7392a0b639e95fa4bd75112b3462edf57ec8..d3c2a3227e131b7d0829f06487d7f6a102d09e3e 100644 (file)
@@ -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],
index 3658ebf5451aefa2d883be70ae35c5ce4e811002..199b9d854d2c101e67ef1e67da18994b94d57308 100644 (file)
@@ -73,9 +73,9 @@
                   <select class="form-select"
                           id="selectedRealm"
                           formControlName="selectedRealm">
-                  <option *ngFor="let realm of realmsInfo"
-                          [value]="realm.realm">
-                        {{ realm.realm }}
+                  <option *ngFor="let realm of realmList"
+                          [value]="realm">
+                        {{ realm }}
                   </option>
                   </select>
                 </div>
 
 <ng-template #progressTemplate>
   <cd-progress [value]="executingTask?.progress"
-               [description]="executingTask?.name?.replace('progress/Multisite-Setup:', '')">
+               [description]="executingTask?.name?.replace('progress/Multisite-Setup:', '')?.split('||')[0]?.trim()"
+               [subDescription]="executingTask?.name?.replace('progress/Multisite-Setup:', '')?.split('||')[1]?.trim()">
   </cd-progress>
 </ng-template>
 
 </ng-template>
 
 <ng-template #reviewTemplate>
+  <cd-alert-panel type="warning"
+                  [showTitle]="false">
+    <span i18n>
+      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.
+    </span>
+  </cd-alert-panel>
   <ng-container [ngSwitch]="multisiteSetupForm.get('configType').value">
     <ng-container *ngSwitchCase="'newRealm'">
       <ng-container *ngTemplateOutlet="newRealmInfo"></ng-container>
index 4872ffb3dbda1770c961874ecff7b7fb94baba97..34773cddbf7b4363358072db697bdb6f95fb13bb 100644 (file)
@@ -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();
     }
   }
 
index 079483ef2003c2525c1326d33d26658119a213e0..03077cd47a8b50fff1823792148de3430fbcdbf1 100644 (file)
@@ -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;
index da9f0cdb05dbfbd9eee8ebe574b13cf88b96cf75..6479b6cd78cb38c7339be6cfa5437a93a08f6577 100644 (file)
@@ -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);
index 940a8b55404166f3a07493636a7ec826a9082423..7826523b5d0ae5ba093cb1794dc18db5ee611160 100644 (file)
@@ -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<void>();
 
   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();
+            }
+          });
       }
-    );
+    });
   }
 }
index 01e4ccb9945a9dd9dbaa892dd88efe9f2b0af10a..27b1e5fe54579078b9a37425624fe46e8c2bcafa 100644 (file)
@@ -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(() => {
index 4063c619e88222d7b985e0ab8598bcadd59ee093..808362b90fb6be2aa785652ebad971edb962cea3 100644 (file)
@@ -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<boolean>(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<boolean> {
+    return this.mgrModuleService.list().pipe(
+      map((moduleData: MgrModuleInfo[]) => {
+        const rgwModule = moduleData.find((module) => module.name === RGW);
+        return !!rgwModule?.enabled;
+      })
+    );
+  }
 }
index e81731cd5203016c1dbe33d131406e350f6de65a..777ff061e147f6c8eab4ffb03a1fb561f90d1244 100644 (file)
@@ -30,8 +30,14 @@ export class RgwRealmService {
     return this.http.put(`${this.url}/${realm.name}`, requestBody);
   }
 
-  list(): Observable<object> {
-    return this.http.get<object>(`${this.url}`);
+  list(replicable?: boolean): Observable<object> {
+    let params = new HttpParams();
+    if (replicable) {
+      params = params.appendAll({
+        replicable: replicable.toString()
+      });
+    }
+    return this.http.get<object>(`${this.url}`, { params });
   }
 
   get(realm: RgwRealm): Observable<object> {
index 9d844576d4305f9efba71c7c39e341d41d8c6395..38dcb13a574cdbb4c0212f22114f97ae26e79bd6 100644 (file)
         *ngIf="description">
       {{ description }}
     </h5>
+
+    <p class="text-center text-muted mt-3"
+       *ngIf="subDescription">
+      {{ subDescription }}
+    </p>
   </ng-container>
 
   <div class="w-50 row h-100 d-flex justify-content-center align-items-center mt-4">
index b39ebf6b30b34a31bd2008438d98274b5a364aa0..b9453da5ada96533406aec3bcebd8844a8704c74 100644 (file)
@@ -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;
index 939b2a5fd20eaa77e9e19f2efd3c2745a0c3f9fe..d65e2aa91ec4885318fdeb6254757ffc26c8df0c 100755 (executable)
@@ -12657,7 +12657,12 @@ paths:
       - RgwMultisite
   /api/rgw/realm:
     get:
-      parameters: []
+      parameters:
+      - allowEmptyValue: true
+        in: query
+        name: replicable
+        schema:
+          type: string
       responses:
         '200':
           content:
index cefad2b045bb7225a5cfa48f55ffbff1136d35dd..081c89ba23ae0f93bbd4dfb5a6574eedc3322312 100755 (executable)
@@ -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]