]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: multisite sync status card for rgw overview dashboard 52915/head
authorAashish Sharma <aasharma@li-e74156cc-2f67-11b2-a85c-e98659a63c5c.ibm.com>
Tue, 25 Jul 2023 12:07:38 +0000 (17:37 +0530)
committerAashish Sharma <aasharma@li-e74156cc-2f67-11b2-a85c-e98659a63c5c.ibm.com>
Mon, 21 Aug 2023 07:18:44 +0000 (12:48 +0530)
Signed-off-by: Aashish Sharma <aasharma@redhat.com>
(cherry picked from commit 1d6f19e53b68c180a2d0301889974949fe899a2c)

31 files changed:
src/pybind/mgr/dashboard/controllers/rgw.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-card-popover.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.scss
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/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/api/rgw-multisite.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/doc.service.ts
src/pybind/mgr/dashboard/frontend/src/styles.scss
src/pybind/mgr/dashboard/frontend/src/styles/defaults/_bootstrap-defaults.scss
src/pybind/mgr/dashboard/services/rgw_client.py

index 3ba4cf4923e4c70e89c32796dc16da24396c2f35..766c8eadc51e5e0dfce117ab8d6dab5c2877c196 100644 (file)
@@ -108,6 +108,14 @@ class RgwMultisiteStatus(RESTController):
                                                          secret_key)
         return result
 
+    @RESTController.Collection(method='GET', path='/sync_status')
+    @allow_empty_body
+    # pylint: disable=W0102,W0613
+    def get_sync_status(self):
+        multisite_instance = RgwMultisite()
+        result = multisite_instance.get_multisite_sync_status()
+        return result
+
 
 @APIRouter('/rgw/daemon', Scope.RGW)
 @APIDoc("RGW Daemon Management API", "RgwDaemon")
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-card-popover.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-card-popover.scss
new file mode 100644 (file)
index 0000000..9192d4e
--- /dev/null
@@ -0,0 +1,20 @@
+@use './src/styles/vendor/variables' as vv;
+
+.rgw-overview-card-popover {
+  max-height: 600px;
+  max-width: 400px;
+  word-break: break-all;
+
+  .popover-body {
+    font-size: 1rem;
+    max-height: 600px;
+    max-width: 400px;
+    overflow: auto;
+
+    li {
+      span {
+        font-size: 1.1em;
+      }
+    }
+  }
+}
index 41efb5d65ca11162c50bee2968d4044faa6b8caa..359e9dd9b853c7731a417f623bdacee94df66cd7 100644 (file)
@@ -92,7 +92,7 @@
       <cd-card cardTitle="Used Capacity"
                i18n-title
                class="col-sm-2 d-flex w-100 h-50 pb-3"
-               aria-label="Details card">
+               aria-label="Used Capacity">
         <span  class="ms-4 me-4 text-center">
           <h1>{{ totalPoolUsedBytes | dimlessBinary}}</h1>
         </span>
       <cd-card cardTitle="Avg Object Size"
                i18n-title
                class="col-sm-2 d-flex w-100 h-50 pt-3"
-               aria-label="Details card">
+               aria-label="Avg Object Size">
         <span class="ms-4 me-4 text-center">
           <h1>{{ averageObjectSize | dimlessBinary}}</h1>
         </span>
       </cd-card>
     </div>
   </div>
+
+  <div class="row pt-4 pb-4">
+    <cd-card cardTitle="Multisite Sync Status"
+             i18n-title>
+      <ng-template #notConfigured>
+        <cd-alert-panel type="info"
+                        i18n>
+          Multisite needs to be configured in order to see the multisite sync status.
+          Please consult the <cd-doc section="multisite"></cd-doc> on how to configure and enable the multisite functionality.
+        </cd-alert-panel>
+      </ng-template>
+      <span *ngIf="loading"
+            class="d-flex justify-content-center">
+        <i [ngClass]="[icons.large3x, icons.spinner, icons.spin]"></i>
+      </span>
+      <div class="row"
+           *ngIf="multisiteSyncStatus$ | async">
+        <div class="row pt-2"
+             *ngIf="showMultisiteCard; else notConfigured">
+          <cd-card cardTitle="Primary Source Zone"
+                   class="col-lg-3 d-flex justify-content-center align-primary-zone">
+            <span *ngIf="loading"
+                  class="d-flex justify-content-center">
+              <i [ngClass]="[icons.large3x, icons.spinner, icons.spin]"></i>
+            </span>
+            <span *ngIf="!loading"
+                  class="d-flex justify-content-center">
+              <cd-rgw-sync-primary-zone [realm]="realm"
+                                        [zonegroup]="zonegroup"
+                                        [zone]="zone">
+              </cd-rgw-sync-primary-zone>
+            </span>
+          </cd-card>
+          <div class="col-lg-9">
+            <cd-card cardTitle="Source Zones"
+                     class="d-flex h-100">
+              <span *ngIf="loading"
+                    class="d-flex justify-content-center">
+                <i [ngClass]="[icons.large3x, icons.spinner, icons.spin]"></i>
+              </span>
+              <div class="row"
+                   *ngIf="!loading">
+                <cd-card *ngFor="let zone of replicaZonesInfo; trackBy: trackByFn"
+                         cardTitle="{{zone.name}}"
+                         cardType="zone"
+                         shadow="true"
+                         i18n-title
+                         class="col-sm-9 col-lg-6 align-replica-zones d-flex pt-4"
+                         aria-label="Source Zones Card">
+                  <div class="row pb-4 ps-3 pe-3">
+                    <cd-card *ngFor="let title of chartTitles"
+                             [cardTitle]="title"
+                             i18n-title
+                             cardType="syncCards"
+                             removeBorder="true"
+                             class="col-sm-9 col-lg-6"
+                             [ngClass]="{ 'border-left': title === 'Data Sync' }"
+                             aria-label="Charts Card">
+                      <span class="me-2 text-center"
+                            *ngIf="title === 'Metadata Sync'">
+                        <cd-rgw-sync-metadata-info [metadataSyncInfo]="metadataSyncInfo">
+                        </cd-rgw-sync-metadata-info>
+                      </span>
+                      <span class="me-2"
+                            *ngIf="title === 'Data Sync'">
+                        <cd-rgw-sync-data-info [zone]="zone">
+                        </cd-rgw-sync-data-info>
+                      </span>
+                    </cd-card>
+                  </div>
+                </cd-card>
+              </div>
+            </cd-card>
+          </div>
+        </div>
+      </div>
+    </cd-card>
+  </div>
 </div>
 
index 39a87125f78c1458ac5776ed6468e14c95ef3b9d..b735edde21f176fb5ea1900bc43339818995f7fe 100644 (file)
@@ -1,3 +1,5 @@
+@use './src/styles/vendor/variables' as vv;
+
 hr {
   margin-bottom: 2px;
   margin-top: 2px;
@@ -6,3 +8,25 @@ hr {
 .list-group-item {
   border: 0;
 }
+
+.align-replica-zones {
+  margin-left: auto;
+  margin-right: auto;
+  padding-left: 2em;
+  padding-right: 2em;
+}
+
+ul {
+  align-items: center;
+  display: flex;
+  flex-direction: column;
+  list-style-type: none;
+}
+
+.align-primary-zone {
+  padding-left: 4em;
+}
+
+.border-left {
+  border-left: 1px solid vv.$chart-color-border;
+}
index 8a2a158f0a3d50f02e263a6968362dfc6555c1c2..b419613d4a07f42b414cff0a52c7f75b8ba4cbd0 100644 (file)
@@ -170,7 +170,7 @@ describe('RgwOverviewDashboardComponent', () => {
   it('should render all cards', () => {
     fixture.detectChanges();
     const dashboardCards = fixture.debugElement.nativeElement.querySelectorAll('cd-card');
-    expect(dashboardCards.length).toBe(4);
+    expect(dashboardCards.length).toBe(5);
   });
 
   it('should get corresponding data into Daemons', () => {
index 81634fe950344a1a73169c44eb929122540cda18..b8c4774bec17f7500cc5541ac266a44178d17b62 100644 (file)
@@ -1,7 +1,7 @@
 import { Component, OnDestroy, OnInit } from '@angular/core';
 
 import _ from 'lodash';
-import { Subscription } from 'rxjs';
+import { Observable, ReplaySubject, Subscription } from 'rxjs';
 
 import { Permissions } from '~/app/shared/models/permissions';
 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
@@ -13,8 +13,13 @@ import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service';
 import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
 import { RgwUserService } from '~/app/shared/api/rgw-user.service';
 import { PrometheusService } from '~/app/shared/api/prometheus.service';
+
 import { RgwPromqls as queries } from '~/app/shared/enum/dashboard-promqls.enum';
 import { HealthService } from '~/app/shared/api/health.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { RgwMultisiteService } from '~/app/shared/api/rgw-multisite.service';
+import { shareReplay, switchMap, tap } from 'rxjs/operators';
+import { RgwZonegroup } from '../models/rgw-multisite';
 
 @Component({
   selector: 'cd-rgw-overview-dashboard',
@@ -22,6 +27,8 @@ import { HealthService } from '~/app/shared/api/health.service';
   styleUrls: ['./rgw-overview-dashboard.component.scss']
 })
 export class RgwOverviewDashboardComponent implements OnInit, OnDestroy {
+  icons = Icons;
+
   interval = new Subscription();
   permissions: Permissions;
   rgwDaemonCount = 0;
@@ -36,6 +43,7 @@ export class RgwOverviewDashboardComponent implements OnInit, OnDestroy {
   realmData: any;
   daemonSub: Subscription;
   realmSub: Subscription;
+  multisiteInfo: object[] = [];
   ZonegroupSub: Subscription;
   ZoneSUb: Subscription;
   UserSub: Subscription;
@@ -48,6 +56,18 @@ export class RgwOverviewDashboardComponent implements OnInit, OnDestroy {
     AVG_PUT_LATENCY: ''
   };
   timerGetPrometheusDataSub: Subscription;
+  chartTitles = ['Metadata Sync', 'Data Sync'];
+  realm: string;
+  zonegroup: string;
+  zone: string;
+  metadataSyncInfo: string;
+  replicaZonesInfo: any = [];
+  metadataSyncData: {};
+  showMultisiteCard = true;
+  loading = true;
+  multisiteSyncStatus$: Observable<any>;
+  subject = new ReplaySubject<any>();
+  syncCardLoading = true;
 
   constructor(
     private authStorageService: AuthStorageService,
@@ -59,7 +79,8 @@ export class RgwOverviewDashboardComponent implements OnInit, OnDestroy {
     private rgwZoneService: RgwZoneService,
     private rgwBucketService: RgwBucketService,
     private rgwUserService: RgwUserService,
-    private prometheusService: PrometheusService
+    private prometheusService: PrometheusService,
+    private rgwMultisiteService: RgwMultisiteService
   ) {
     this.permissions = this.authStorageService.getPermissions();
   }
@@ -80,6 +101,7 @@ export class RgwOverviewDashboardComponent implements OnInit, OnDestroy {
         this.totalPoolUsedBytes = data['total_pool_bytes_used'];
         this.averageObjectSize = data['average_object_size'];
       });
+      this.getSyncStatus();
     });
     this.realmSub = this.rgwRealmService.list().subscribe((data: any) => {
       this.rgwRealmCount = data['realms'].length;
@@ -91,6 +113,27 @@ export class RgwOverviewDashboardComponent implements OnInit, OnDestroy {
       this.rgwZoneCount = data['zones'].length;
     });
     this.getPrometheusData(this.prometheusService.lastHourDateObject);
+    this.multisiteSyncStatus$ = this.subject.pipe(
+      switchMap(() =>
+        this.rgwMultisiteService.getSyncStatus().pipe(
+          tap((data: any) => {
+            this.loading = false;
+            this.replicaZonesInfo = data['dataSyncInfo'];
+            this.metadataSyncInfo = data['metadataSyncInfo'];
+            [this.realm, this.zonegroup, this.zone] = data['primaryZoneData'];
+          })
+        )
+      ),
+      tap(() => {
+        const zonegroup = new RgwZonegroup();
+        zonegroup.name = this.zonegroup;
+        this.rgwZonegroupService.get(zonegroup).subscribe((data: any) => {
+          this.showMultisiteCard = data['zones'].length !== 1;
+          this.syncCardLoading = false;
+        });
+      }),
+      shareReplay(1)
+    );
   }
 
   ngOnDestroy() {
@@ -115,4 +158,12 @@ export class RgwOverviewDashboardComponent implements OnInit, OnDestroy {
       true
     );
   }
+
+  getSyncStatus() {
+    this.subject.next();
+  }
+
+  trackByFn(zone: any) {
+    return zone;
+  }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.html
new file mode 100644 (file)
index 0000000..f6e2479
--- /dev/null
@@ -0,0 +1,63 @@
+<ng-template #syncPopover>
+  <ul class="text-center">
+    <li><h5><b>Sync Status:</b></h5></li>
+    <li *ngFor="let status of zone.fullSyncStatus">
+      <span *ngIf="!status?.includes(zone.name) && !status?.includes(zone.syncstatus) && !status?.includes('failed') && !status?.includes('error')">
+        <span *ngIf="status?.includes(':')">
+          <b>{{ status.split(':')[0] | titlecase }}</b>:{{ status.split(':')[1] | titlecase}}
+        </span>
+        <span *ngIf="!status?.includes(':')">
+          <b>{{ status | titlecase }}</b>
+        </span>
+      </span>
+      <span *ngIf="status?.includes('failed') || status?.includes('error')">
+        {{ status | titlecase }}
+      </span>
+    </li>
+  </ul>
+</ng-template>
+<ul class="me-2">
+  <ng-template #showStatus>
+    <a *ngIf="zone.syncstatus !== 'Not Syncing From Zone'"
+       class="lead text-primary"
+       [ngbPopover]="syncPopover"
+       placement="top"
+       popoverClass="rgw-overview-card-popover"
+       i18n>{{ zone.syncstatus | titlecase }}</a>
+    <a *ngIf="zone.syncstatus === 'Not Syncing From Zone'"
+       class="lead text-primary"
+       [ngbPopover]="syncPopover"
+       placement="top"
+       popoverClass="rgw-overview-card-popover"
+       i18n>Not Syncing</a>
+  </ng-template>
+  <li><b>Status:</b></li>
+  <li *ngIf="zone.syncstatus?.includes('failed') || zone.syncstatus?.includes('error'); else showStatus">
+    <i [ngClass]="[icons.danger]"
+       class="text-danger"></i>
+    <a class="lead text-danger"
+       [ngbPopover]="syncPopover"
+       placement="top"
+       popoverClass="rgw-overview-card-popover"
+       i18n>Error</a></li>
+  <li class="mt-4 w-100 text-center"
+      *ngIf="zone.syncstatus === 'preparing for full sync'">
+    <b>Full sync progress:</b>
+    <cd-usage-bar *ngIf="zone.fullSync"
+                  [total]="zone.fullSync[1]"
+                  [showMultisiteTooltip]="true"
+                  [used]="zone.fullSync[0]"
+                  [title]="shards"
+                  decimals="2">
+    </cd-usage-bar></li>
+  <li class="mt-4 w-100 text-center"
+      *ngIf="zone.incrementalSync">
+  <b>Sync Progress:</b>
+  <cd-usage-bar *ngIf="zone.incrementalSync"
+                [total]="zone.totalShards"
+                [showMultisiteTooltip]="true"
+                [used]="zone.usedShards"
+                [title]="shards"
+                decimals="2">
+  </cd-usage-bar></li>
+</ul>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.scss
new file mode 100644 (file)
index 0000000..4386b0c
--- /dev/null
@@ -0,0 +1,8 @@
+@use './src/styles/vendor/variables' as vv;
+
+ul {
+  align-items: center;
+  display: flex;
+  flex-direction: column;
+  list-style-type: none;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.spec.ts
new file mode 100644 (file)
index 0000000..47fb26d
--- /dev/null
@@ -0,0 +1,26 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwSyncDataInfoComponent } from './rgw-sync-data-info.component';
+import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
+
+describe('RgwSyncDataInfoComponent', () => {
+  let component: RgwSyncDataInfoComponent;
+  let fixture: ComponentFixture<RgwSyncDataInfoComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [RgwSyncDataInfoComponent],
+      imports: [NgbPopoverModule]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(RgwSyncDataInfoComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.ts
new file mode 100644 (file)
index 0000000..a7ec87d
--- /dev/null
@@ -0,0 +1,16 @@
+import { Component, Input } from '@angular/core';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+  selector: 'cd-rgw-sync-data-info',
+  templateUrl: './rgw-sync-data-info.component.html',
+  styleUrls: ['./rgw-sync-data-info.component.scss']
+})
+export class RgwSyncDataInfoComponent {
+  icons = Icons;
+
+  @Input()
+  zone: any = {};
+
+  constructor() {}
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.html
new file mode 100644 (file)
index 0000000..cf095c6
--- /dev/null
@@ -0,0 +1,70 @@
+<span *ngIf="metadataSyncInfo === 'no sync (zone is master)'">
+  <ul class="me-2">
+    <li><b>Status:</b></li>
+    <li>No Sync</li>
+  </ul>
+</span>
+<span *ngIf="metadataSyncInfo !== 'no sync (zone is master)'">
+  <ng-template #metadataSyncPopover>
+    <ul class="text-center">
+      <li *ngFor="let status of metadataSyncInfo.fullSyncStatus">
+        <span *ngIf="!status?.includes(metadataSyncInfo.syncstatus) && !status?.includes('failed') && !status?.includes('error')">
+          <span *ngIf="status?.includes(':')">
+            <b>{{ status.split(':')[0] | titlecase }}</b>:{{ status.split(':')[1] | titlecase}}
+          </span>
+          <span *ngIf="!status?.includes(':')">
+            <b>{{ status | titlecase }}</b>
+          </span>
+        </span>
+        <span *ngIf="status?.includes('failed') || status?.includes('error')">
+            {{ status | titlecase }}
+        </span>
+      </li>
+    </ul>
+  </ng-template>
+  <ul class="me-2">
+    <ng-template #showMetadataStatus>
+      <a *ngIf="metadataSyncInfo.syncstatus !== 'Not Syncing From Zone'"
+         class="lead text-primary"
+         [ngbPopover]="metadataSyncPopover"
+         placement="top"
+         popoverClass="rgw-overview-card-popover"
+         i18n>{{ metadataSyncInfo.syncstatus | titlecase }}</a>
+      <a *ngIf="metadataSyncInfo.syncstatus === 'Not Syncing From Zone'"
+         class="lead text-primary"
+         [ngbPopover]="metadataSyncPopover"
+         placement="top"
+         popoverClass="rgw-overview-card-popover"
+         i18n>Not Syncing</a>
+    </ng-template>
+    <li><b>Status:</b></li>
+    <li *ngIf="metadataSyncInfo.syncstatus?.includes('failed') || metadataSyncInfo.syncstatus?.includes('error'); else showMetadataStatus">
+      <i class="text-danger"
+         [ngClass]="[icons.danger]"></i>
+      <a class="lead text-danger"
+         [ngbPopover]="metadataSyncPopover"
+         placement="top"
+         popoverClass="rgw-overview-card-popover"
+         i18n>Error</a></li>
+    <li class="mt-4 setwidth text-center"
+        *ngIf="metadataSyncInfo.syncstatus === 'preparing for full sync'">
+      <b>Full sync progress:</b>
+    <cd-usage-bar *ngIf="metadataSyncInfo.fullSync"
+                  [total]="metadataSyncInfo.fullSync[1]"
+                  [showMultisiteTooltip]="true"
+                  [used]="metadataSyncInfo.fullSync[0]"
+                  [title]="shards"
+                  decimals="2">
+    </cd-usage-bar></li>
+    <li class="mt-4 setwidth text-center"
+        *ngIf="metadataSyncInfo.incrementalSync">
+    <b>Sync Progress:</b>
+    <cd-usage-bar *ngIf="metadataSyncInfo.incrementalSync"
+                  [total]="metadataSyncInfo.totalShards"
+                  [showMultisiteTooltip]="true"
+                  [used]="metadataSyncInfo.usedShards"
+                  [title]="shards"
+                  decimals="2">
+    </cd-usage-bar></li>
+  </ul>
+</span>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.scss
new file mode 100644 (file)
index 0000000..4386b0c
--- /dev/null
@@ -0,0 +1,8 @@
+@use './src/styles/vendor/variables' as vv;
+
+ul {
+  align-items: center;
+  display: flex;
+  flex-direction: column;
+  list-style-type: none;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.spec.ts
new file mode 100644 (file)
index 0000000..89060fd
--- /dev/null
@@ -0,0 +1,26 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwSyncMetadataInfoComponent } from './rgw-sync-metadata-info.component';
+import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
+
+describe('RgwSyncMetadataInfoComponent', () => {
+  let component: RgwSyncMetadataInfoComponent;
+  let fixture: ComponentFixture<RgwSyncMetadataInfoComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [RgwSyncMetadataInfoComponent],
+      imports: [NgbPopoverModule]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(RgwSyncMetadataInfoComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.ts
new file mode 100644 (file)
index 0000000..bf05c19
--- /dev/null
@@ -0,0 +1,16 @@
+import { Component, Input } from '@angular/core';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+  selector: 'cd-rgw-sync-metadata-info',
+  templateUrl: './rgw-sync-metadata-info.component.html',
+  styleUrls: ['./rgw-sync-metadata-info.component.scss']
+})
+export class RgwSyncMetadataInfoComponent {
+  icons = Icons;
+
+  @Input()
+  metadataSyncInfo: any = {};
+
+  constructor() {}
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.html
new file mode 100644 (file)
index 0000000..f0e0457
--- /dev/null
@@ -0,0 +1,15 @@
+<ul class="pb-5">
+  <li><i [ngClass]="[icons.large2x, icons.reweight]"
+         class="pt-2"></i></li>
+  <li class="badge badge-info mt-2">{{realm}}</li>
+  <li><i [ngClass]="[icons.large2x, icons.down]"
+         class="mt-2"></i></li>
+  <li><i [ngClass]="[icons.large2x, icons.cubes]"
+         class="mt-2"></i></li>
+  <p class="badge badge-info mt-2">{{zonegroup}}</p>
+  <li><i [ngClass]="[icons.large2x, icons.down]"
+         class="mt-2"></i></li>
+  <li><i [ngClass]="[icons.large2x, icons.deploy]"
+         class="mt-2"></i></li>
+  <li class="badge badge-info mt-2">{{zone}}</li>
+</ul>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.scss
new file mode 100644 (file)
index 0000000..795ecec
--- /dev/null
@@ -0,0 +1,12 @@
+@use './src/styles/vendor/variables' as vv;
+
+ul {
+  align-items: center;
+  display: flex;
+  flex-direction: column;
+  list-style-type: none;
+}
+
+.align-primary-zone {
+  padding-left: 4em;
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.spec.ts
new file mode 100644 (file)
index 0000000..682065a
--- /dev/null
@@ -0,0 +1,24 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RgwSyncPrimaryZoneComponent } from './rgw-sync-primary-zone.component';
+
+describe('RgwSyncPrimaryZoneComponent', () => {
+  let component: RgwSyncPrimaryZoneComponent;
+  let fixture: ComponentFixture<RgwSyncPrimaryZoneComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [RgwSyncPrimaryZoneComponent]
+    }).compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(RgwSyncPrimaryZoneComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.ts
new file mode 100644 (file)
index 0000000..483ac1f
--- /dev/null
@@ -0,0 +1,22 @@
+import { Component, Input } from '@angular/core';
+import { Icons } from '~/app/shared/enum/icons.enum';
+
+@Component({
+  selector: 'cd-rgw-sync-primary-zone',
+  templateUrl: './rgw-sync-primary-zone.component.html',
+  styleUrls: ['./rgw-sync-primary-zone.component.scss']
+})
+export class RgwSyncPrimaryZoneComponent {
+  icons = Icons;
+
+  @Input()
+  realm: string;
+
+  @Input()
+  zonegroup: string;
+
+  @Input()
+  zone: string;
+
+  constructor() {}
+}
index fa0e72584b90019af9f3dccd16187ad7d531890a..0028b1bb8d873de1d08eb9c5ce66dca5982f1214 100644 (file)
@@ -3,7 +3,7 @@ import { NgModule } from '@angular/core';
 import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { RouterModule, Routes } from '@angular/router';
 
-import { NgbNavModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
+import { NgbNavModule, NgbPopoverModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
 import { NgxPipeFunctionModule } from 'ngx-pipe-function';
 
 import { ActionLabels, URLVerbs } from '~/app/shared/constants/app.constants';
@@ -43,6 +43,9 @@ import { RgwMultisiteExportComponent } from './rgw-multisite-export/rgw-multisit
 import { CreateRgwServiceEntitiesComponent } from './create-rgw-service-entities/create-rgw-service-entities.component';
 import { RgwOverviewDashboardComponent } from './rgw-overview-dashboard/rgw-overview-dashboard.component';
 import { DashboardV3Module } from '../dashboard-v3/dashboard-v3.module';
+import { RgwSyncPrimaryZoneComponent } from './rgw-sync-primary-zone/rgw-sync-primary-zone.component';
+import { RgwSyncMetadataInfoComponent } from './rgw-sync-metadata-info/rgw-sync-metadata-info.component';
+import { RgwSyncDataInfoComponent } from './rgw-sync-data-info/rgw-sync-data-info.component';
 
 @NgModule({
   imports: [
@@ -54,6 +57,7 @@ import { DashboardV3Module } from '../dashboard-v3/dashboard-v3.module';
     NgbNavModule,
     RouterModule,
     NgbTooltipModule,
+    NgbPopoverModule,
     NgxPipeFunctionModule,
     TreeModule,
     DataTableModule,
@@ -95,7 +99,10 @@ import { DashboardV3Module } from '../dashboard-v3/dashboard-v3.module';
     RgwMultisiteImportComponent,
     RgwMultisiteExportComponent,
     CreateRgwServiceEntitiesComponent,
-    RgwOverviewDashboardComponent
+    RgwOverviewDashboardComponent,
+    RgwSyncPrimaryZoneComponent,
+    RgwSyncMetadataInfoComponent,
+    RgwSyncDataInfoComponent
   ]
 })
 export class RgwModule {}
index d8c1891fc2129f79c88885d536f395a8dc3def63..fe3bfc6acf9ea639f657791daa325b36d32d1e84 100644 (file)
@@ -1,7 +1,7 @@
 <block-ui>
   <cd-navigation>
     <div class="container-fluid h-100"
-         [ngClass]="{'dashboard': (router.url == '/dashboard' || router.url == '/dashboard_3')}">
+         [ngClass]="{'dashboard': (router.url == '/dashboard' || router.url == '/dashboard_3'), 'rgw-dashboard': (router.url == '/rgw/overview')}">
     <cd-context></cd-context>
       <cd-breadcrumbs></cd-breadcrumbs>
       <router-outlet></router-outlet>
index fbd2ad64ec45dad2149816d5e6890861c53ed336..d36c3a29e1a955615a3e25ea1b2aec80887c94cf 100644 (file)
@@ -25,4 +25,8 @@ export class RgwMultisiteService {
       return this.http.put(`${this.url}/migrate`, null, { params: params });
     });
   }
+
+  getSyncStatus() {
+    return this.http.get(`${this.url}/sync_status`);
+  }
 }
index a2f5b9d3d240d0f0d67ffac8772c3d9a9effe517..ba258a285ed950cd5cffa7d886097a2655088d08 100644 (file)
@@ -1,7 +1,18 @@
-<div class="card shadow-sm flex-fill">
-  <h4 class="card-title mt-4 ms-4 mb-0">
-    {{ cardTitle }}
+<div class="card flex-fill"
+     [ngClass]="{'border-0': removeBorder, 'bg-color': cardType === 'Sync Status Card', 'shadow': shadow, 'shadow-sm': !shadow && cardType !== 'syncCards'}">
+  <h4 class="card-title mt-4 ms-4 mb-0"
+      *ngIf="cardType !== 'zone'">
+    <span *ngIf="cardType === ''">{{ cardTitle }}</span>
   </h4>
+  <h4 *ngIf="cardType === 'zone'"
+      class="text-center mt-4 mb-0">
+    <i [ngClass]="icons.deploy"></i>
+    <span class="badge badge-info">{{ cardTitle }}</span>
+  </h4>
+  <h5 *ngIf="cardType === 'syncCards'"
+      class="text-center card-title">
+      {{ cardTitle }}
+  </h5>
   <div class="card-body ps-0 pe-0">
     <ng-content></ng-content>
   </div>
index 8e93cc8645c854589dbb7d7f44fff72e01bb3fb7..9123b48fb3791eb933889f72d78f0276e3961f18 100644 (file)
@@ -1,4 +1,5 @@
 import { Component, Input } from '@angular/core';
+import { Icons } from '~/app/shared/enum/icons.enum';
 
 @Component({
   selector: 'cd-card',
@@ -6,6 +7,14 @@ import { Component, Input } from '@angular/core';
   styleUrls: ['./card.component.scss']
 })
 export class CardComponent {
+  icons = Icons;
+
   @Input()
   cardTitle: string;
+  @Input()
+  cardType: string = '';
+  @Input()
+  removeBorder = false;
+  @Input()
+  shadow = false;
 }
index 70020436edecf6878ee77c7ce3dc02916b475011..a97604c97342a92303ae111cf91818d721e731f7 100644 (file)
@@ -1,5 +1,5 @@
 <ng-template #usageTooltipTpl>
-  <table>
+  <table *ngIf="!showMultisiteTooltip">
     <tr>
       <td class="text-left me-1">Used:</td>
       <td class="text-right"><strong> {{ isBinary ? (used | dimlessBinary) : (used | dimless) }}</strong></td>
       <td class="text-right"><strong>{{ isBinary ? (customLegendValue | dimlessBinary) : (customLegend[1] | dimless) }}</strong></td>
     </tr>
   </table>
+  <table *ngIf="showMultisiteTooltip">
+    <tr>
+      <td class="text-left">Total Shards:&nbsp;</td>
+      <td class="text-right"><strong> {{ total }}</strong></td>
+    </tr>
+    <tr *ngIf="calculatePerc">
+      <td class="text-left">Transferred Shards:&nbsp;</td>
+      <td class="'text-right"><strong>{{ used }}</strong></td>
+    </tr>
+  </table>
 </ng-template>
 
 <div class="progress"
index e9d6d24984db1af9ece937bddd0d72dd9a7411a0..3c57015fec799f2421c59c1244f75c6a66e3490e 100644 (file)
@@ -26,7 +26,7 @@
   }
 
   span {
-    color: vv.$black;
+    color: vv.$white;
     display: block;
     font-weight: normal;
     position: absolute;
index 4940c19061ba32e768d1492d63f2ffd883d38fd2..58bd7d4a46dde15064ed90f9e76b9e2d2eb35a91 100644 (file)
@@ -30,6 +30,8 @@ export class UsageBarComponent implements OnChanges {
   customLegendValue?: string;
   @Input()
   showFreeToolTip = true;
+  @Input()
+  showMultisiteTooltip = false;
 
   usedPercentage: number;
   freePercentage: number;
index 00c91abd092628b3b45ce39837632d66a67c5d05..d5bcbb1f06abd90e537600a0c08d85ce54293faf 100644 (file)
@@ -36,6 +36,7 @@ export class DocService {
       'rgw-nfs': `${domain}radosgw/nfs`,
       rgw: `${domain}mgr/dashboard/#enabling-the-object-gateway-management-frontend`,
       'rgw-multisite': `${domain}/radosgw/multisite/#failover-and-disaster-recovery`,
+      multisite: `${domain}/radosgw/multisite`,
       dashboard: `${domain}mgr/dashboard`,
       grafana: `${domain}mgr/dashboard/#enabling-the-embedding-of-grafana-dashboards`,
       orch: `${domain}mgr/orchestrator`,
index fc6a9a25446780f6e8b53f3866b3a1a53ba0a494..6c73866e1937d3d862c371425d7fd77eb2227441 100644 (file)
@@ -26,6 +26,7 @@ $grid-breakpoints: (
 @import '~bootstrap/scss/bootstrap';
 @import '~fork-awesome/scss/fork-awesome';
 @import 'app/ceph/dashboard/info-card/info-card-popover.scss';
+@import 'app/ceph/rgw/rgw-overview-dashboard/rgw-overview-card-popover.scss';
 @import './src/styles/bootstrap-extends';
 
 @import './src/styles/ceph-custom/basics';
index 5eed9eb17905d798e83fbaf669babd1164a3b3b8..4be14a898784f1e2c99dfddd6afe0c71d2b2c7d8 100644 (file)
@@ -93,6 +93,7 @@ $chart-color-tooltip-background: $black !default;
 $chart-danger: #c9190b !default;
 $chart-color-strong-blue: #0078c8 !default;
 $chart-color-translucent-blue: #0096dc80 !default;
+$chart-color-border: #00000020 !default;
 
 // Typography
 
index 707612acf837b9f34dd4c1142eba91971ac462cd..bfcc2bef0c2199391b0b5019b3d048b8a9899035 100644 (file)
@@ -1528,3 +1528,130 @@ class RgwMultisite:
                 and len(rgw_zone_list['zones']) < 1:
             is_multisite_configured = False
         return is_multisite_configured
+
+    def get_multisite_sync_status(self):
+        rgw_multisite_sync_status_cmd = ['sync', 'status']
+        try:
+            exit_code, out, _ = mgr.send_rgwadmin_command(rgw_multisite_sync_status_cmd, False)
+            if exit_code > 0:
+                raise DashboardException('Unable to get multisite sync status',
+                                         http_status_code=500, component='rgw')
+            if out:
+                return self.process_data(out)
+        except SubprocessError as error:
+            raise DashboardException(error, http_status_code=500, component='rgw')
+        return {}
+
+    def process_data(self, data):
+        primary_zone_data, metadata_sync_data = self.extract_metadata_and_primary_zone_data(data)
+        datasync_info = self.extract_datasync_info(data)
+        replica_zones_info = [self.extract_replica_zone_data(item) for item in datasync_info]
+
+        replica_zones_info_object = {
+            'metadataSyncInfo': metadata_sync_data,
+            'dataSyncInfo': replica_zones_info,
+            'primaryZoneData': primary_zone_data
+        }
+
+        return replica_zones_info_object
+
+    def extract_metadata_and_primary_zone_data(self, data):
+        primary_zone_info, metadata_sync_infoormation = self.extract_zones_data(data)
+
+        primary_zone_tree = primary_zone_info.split('\n') if primary_zone_info else []
+        realm = self.get_primary_zonedata(primary_zone_tree[0])
+        zonegroup = self.get_primary_zonedata(primary_zone_tree[1])
+        zone = self.get_primary_zonedata(primary_zone_tree[2])
+
+        primary_zone_data = [realm, zonegroup, zone]
+        metadata_sync_data = self.extract_metadata_sync_data(metadata_sync_infoormation)
+
+        return primary_zone_data, metadata_sync_data
+
+    def extract_zones_data(self, data):
+        result = data
+        primary_zone_info = result.split('metadata sync')[0] if 'metadata sync' in result else None
+        metadata_sync_infoormation = result.split('metadata sync')[1] if 'metadata sync' in result else None  # noqa E501  #pylint: disable=line-too-long
+        return primary_zone_info, metadata_sync_infoormation
+
+    def extract_metadata_sync_data(self, metadata_sync_infoormation):
+        metadata_sync_info = metadata_sync_infoormation.split('data sync source')[0].strip() if 'data sync source' in metadata_sync_infoormation else None  # noqa E501  #pylint: disable=line-too-long
+
+        if metadata_sync_info == 'no sync (zone is master)':
+            return metadata_sync_info
+
+        metadata_sync_data = {}
+        metadata_sync_info_array = metadata_sync_info.split('\n') if metadata_sync_info else []
+        metadata_sync_data['syncstatus'] = metadata_sync_info_array[1].strip() if len(metadata_sync_info_array) > 1 else None  # noqa E501  #pylint: disable=line-too-long
+
+        for item in metadata_sync_info_array:
+            self.extract_metadata_sync_info(metadata_sync_data, item)
+
+        metadata_sync_data['totalShards'] = metadata_sync_data['incrementalSync'][1] if len(metadata_sync_data['incrementalSync']) > 1 else 0  # noqa E501  #pylint: disable=line-too-long
+        metadata_sync_data['usedShards'] = int(metadata_sync_data['incrementalSync'][1]) - int(metadata_sync_data['behindShards'])  # noqa E501  #pylint: disable=line-too-long
+        return metadata_sync_data
+
+    def extract_metadata_sync_info(self, metadata_sync_data, item):
+        if 'full sync' in item and item.endswith('shards'):
+            metadata_sync_data['fullSync'] = self.get_shards_info(item.strip()).split('/')
+        elif 'incremental sync' in item:
+            metadata_sync_data['incrementalSync'] = self.get_shards_info(item.strip()).split('/')
+        elif 'data is behind' in item or 'data is caught up' in item:
+            metadata_sync_data['dataSyncStatus'] = item.strip()
+
+            if 'data is behind' in item:
+                metadata_sync_data['behindShards'] = self.get_behind_shards(item)
+
+    def extract_datasync_info(self, data):
+        metadata_sync_infoormation = data.split('metadata sync')[1] if 'metadata sync' in data else None  # noqa E501  #pylint: disable=line-too-long
+        if 'data sync source' in metadata_sync_infoormation:
+            datasync_info = metadata_sync_infoormation.split('data sync source')[1].split('source:')
+            return datasync_info
+        return []
+
+    def extract_replica_zone_data(self, datasync_item):
+        replica_zone_data = {}
+        datasync_info_array = datasync_item.split('\n')
+        replica_zone_name = self.get_primary_zonedata(datasync_info_array[0])
+        replica_zone_data['name'] = replica_zone_name.strip()
+        replica_zone_data['syncstatus'] = datasync_info_array[1].strip()
+        replica_zone_data['fullSyncStatus'] = datasync_info_array
+        for item in datasync_info_array:
+            self.extract_metadata_sync_info(replica_zone_data, item)
+
+        if 'incrementalSync' in replica_zone_data:
+            replica_zone_data['totalShards'] = int(replica_zone_data['incrementalSync'][1]) if len(replica_zone_data['incrementalSync']) > 1 else 0  # noqa E501  #pylint: disable=line-too-long
+
+            if 'behindShards' in replica_zone_data:
+                replica_zone_data['usedShards'] = (int(replica_zone_data['incrementalSync'][1]) - int(replica_zone_data['behindShards'])) if len(replica_zone_data['incrementalSync']) > 1 else 0  # noqa E501  #pylint: disable=line-too-long
+            else:
+                replica_zone_data['usedShards'] = replica_zone_data['totalShards']
+
+        return replica_zone_data
+
+    def get_primary_zonedata(self, data):
+        regex = r'\(([^)]+)\)'
+        match = re.search(regex, data)
+
+        if match and match.group(1):
+            return match.group(1)
+
+        return ''
+
+    def get_shards_info(self, data):
+        regex = r'\d+/\d+'
+        match = re.search(regex, data)
+
+        if match:
+            return match.group(0)
+
+        return None
+
+    def get_behind_shards(self, data):
+        regex = r'on\s+(\d+)\s+shards'
+        match = re.search(regex, data, re.IGNORECASE)
+
+        if match:
+            return match.group(1)
+
+        return None