From 689d74777bd9cf51f81d32ffb88e11283260e763 Mon Sep 17 00:00:00 2001 From: Aashish Sharma Date: Tue, 25 Jul 2023 17:37:38 +0530 Subject: [PATCH] mgr/dashboard: multisite sync status card for rgw overview dashboard Signed-off-by: Aashish Sharma (cherry picked from commit 1d6f19e53b68c180a2d0301889974949fe899a2c) --- src/pybind/mgr/dashboard/controllers/rgw.py | 8 ++ .../ceph/dashboard-v3/dashboard-v3.module.ts | 8 +- .../rgw-overview-card-popover.scss | 20 +++ .../rgw-overview-dashboard.component.html | 82 ++++++++++- .../rgw-overview-dashboard.component.scss | 24 ++++ .../rgw-overview-dashboard.component.spec.ts | 6 +- .../rgw-overview-dashboard.component.ts | 55 +++++++- .../rgw-sync-data-info.component.html | 63 +++++++++ .../rgw-sync-data-info.component.scss | 8 ++ .../rgw-sync-data-info.component.spec.ts | 26 ++++ .../rgw-sync-data-info.component.ts | 16 +++ .../rgw-sync-metadata-info.component.html | 70 ++++++++++ .../rgw-sync-metadata-info.component.scss | 8 ++ .../rgw-sync-metadata-info.component.spec.ts | 26 ++++ .../rgw-sync-metadata-info.component.ts | 16 +++ .../rgw-sync-primary-zone.component.html | 15 +++ .../rgw-sync-primary-zone.component.scss | 12 ++ .../rgw-sync-primary-zone.component.spec.ts | 24 ++++ .../rgw-sync-primary-zone.component.ts | 22 +++ .../frontend/src/app/ceph/rgw/rgw.module.ts | 11 +- .../workbench-layout.component.html | 2 +- .../workbench-layout.component.scss | 4 + .../app/shared/api/rgw-multisite.service.ts | 4 + .../components/card/card.component.html | 17 ++- .../shared/components/card/card.component.ts | 9 ++ .../usage-bar/usage-bar.component.html | 12 +- .../usage-bar/usage-bar.component.scss | 2 +- .../usage-bar/usage-bar.component.ts | 2 + .../src/app/shared/services/doc.service.ts | 1 + .../mgr/dashboard/frontend/src/styles.scss | 1 + .../styles/defaults/_bootstrap-defaults.scss | 1 + .../mgr/dashboard/services/rgw_client.py | 127 ++++++++++++++++++ 32 files changed, 680 insertions(+), 22 deletions(-) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-card-popover.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.html create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.scss create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.ts diff --git a/src/pybind/mgr/dashboard/controllers/rgw.py b/src/pybind/mgr/dashboard/controllers/rgw.py index 3ba4cf4923e..766c8eadc51 100644 --- a/src/pybind/mgr/dashboard/controllers/rgw.py +++ b/src/pybind/mgr/dashboard/controllers/rgw.py @@ -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/dashboard-v3/dashboard-v3.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts index 9b311a1d489..50db430906e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard-v3.module.ts @@ -38,12 +38,6 @@ import { PgSummaryPipe } from './pg-summary.pipe'; DashboardTimeSelectorComponent ], - exports: [ - DashboardV3Component, - CardComponent, - CardRowComponent, - DashboardAreaChartComponent, - DashboardTimeSelectorComponent - ] + exports: [DashboardV3Component, DashboardAreaChartComponent, DashboardTimeSelectorComponent] }) export class DashboardV3Module {} 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 index 00000000000..9192d4eb981 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-card-popover.scss @@ -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; + } + } + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html index 41efb5d65ca..359e9dd9b85 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.html @@ -92,7 +92,7 @@ + aria-label="Used Capacity">

{{ totalPoolUsedBytes | dimlessBinary}}

@@ -100,12 +100,90 @@ + aria-label="Avg Object Size">

{{ averageObjectSize | dimlessBinary}}

+ +
+ + + + Multisite needs to be configured in order to see the multisite sync status. + Please consult the on how to configure and enable the multisite functionality. + + + + + +
+
+ + + + + + + + + +
+ + + + +
+ +
+ + + + + + + + + + +
+
+
+
+
+
+
+
+
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.scss index 39a87125f78..b735edde21f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.scss @@ -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; +} 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 f887bcd8224..1f759abd934 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 @@ -5,8 +5,6 @@ import { of } from 'rxjs'; import { RgwDaemonService } from '~/app/shared/api/rgw-daemon.service'; import { RgwDaemon } from '../models/rgw-daemon'; import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { CardComponent } from '../../dashboard-v3/card/card.component'; -import { CardRowComponent } from '../../dashboard-v3/card-row/card-row.component'; import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe'; import { RgwRealmService } from '~/app/shared/api/rgw-realm.service'; import { RgwZonegroupService } from '~/app/shared/api/rgw-zonegroup.service'; @@ -14,6 +12,8 @@ import { RgwZoneService } from '~/app/shared/api/rgw-zone.service'; import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service'; import { RgwUserService } from '~/app/shared/api/rgw-user.service'; import { HealthService } from '~/app/shared/api/health.service'; +import { CardComponent } from '~/app/shared/components/card/card.component'; +import { CardRowComponent } from '~/app/shared/components/card-row/card-row.component'; describe('RgwOverviewDashboardComponent', () => { let component: RgwOverviewDashboardComponent; @@ -168,7 +168,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', () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.ts index 81634fe9503..b8c4774bec1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-overview-dashboard/rgw-overview-dashboard.component.ts @@ -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; + subject = new ReplaySubject(); + 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 index 00000000000..f6e247983db --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.html @@ -0,0 +1,63 @@ + +
    +
  • Sync Status:
  • +
  • + + + {{ status.split(':')[0] | titlecase }}:{{ status.split(':')[1] | titlecase}} + + + {{ status | titlecase }} + + + + {{ status | titlecase }} + +
  • +
+
+ 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 index 00000000000..4386b0c6157 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.scss @@ -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 index 00000000000..47fb26d8dea --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.spec.ts @@ -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; + + 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 index 00000000000..a7ec87da079 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-data-info/rgw-sync-data-info.component.ts @@ -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 index 00000000000..cf095c6ebf5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.html @@ -0,0 +1,70 @@ + +
    +
  • Status:
  • +
  • No Sync
  • +
+
+ + +
    +
  • + + + {{ status.split(':')[0] | titlecase }}:{{ status.split(':')[1] | titlecase}} + + + {{ status | titlecase }} + + + + {{ status | titlecase }} + +
  • +
+
+ +
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 index 00000000000..4386b0c6157 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.scss @@ -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 index 00000000000..89060fd5d25 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.spec.ts @@ -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; + + 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 index 00000000000..bf05c194a39 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-metadata-info/rgw-sync-metadata-info.component.ts @@ -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 index 00000000000..f0e0457d944 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.html @@ -0,0 +1,15 @@ +
    +
  • +
  • {{realm}}
  • +
  • +
  • +

    {{zonegroup}}

    +
  • +
  • +
  • {{zone}}
  • +
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 index 00000000000..795ecec64a4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.scss @@ -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 index 00000000000..682065a71c4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.spec.ts @@ -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; + + 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 index 00000000000..483ac1fcf67 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw-sync-primary-zone/rgw-sync-primary-zone.component.ts @@ -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() {} +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts index 20158b797f2..05c1c1af228 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/rgw/rgw.module.ts @@ -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 {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html index d8c1891fc21..fe3bfc6acf9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.html @@ -1,7 +1,7 @@
+ [ngClass]="{'dashboard': (router.url == '/dashboard' || router.url == '/dashboard_3'), 'rgw-dashboard': (router.url == '/rgw/overview')}"> diff --git a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.scss index 7ec90d43ec9..32c0b2ae8c0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/core/layouts/workbench-layout/workbench-layout.component.scss @@ -10,3 +10,7 @@ overflow: auto; position: absolute; } + +.rgw-dashboard { + background-color: vv.$body-bg-alt; +} 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 fbd2ad64ec4..d36c3a29e1a 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 @@ -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`); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.html index a2f5b9d3d24..ba258a285ed 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.html @@ -1,7 +1,18 @@ -
-

- {{ cardTitle }} +
+

+ {{ cardTitle }}

+

+ + {{ cardTitle }} +

+
+ {{ cardTitle }} +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.ts index 8e93cc8645c..9123b48fb37 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/card/card.component.ts @@ -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; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html index 70020436ede..a97604c9734 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/usage-bar/usage-bar.component.html @@ -1,5 +1,5 @@ - +
@@ -13,6 +13,16 @@
Used: {{ isBinary ? (used | dimlessBinary) : (used | dimless) }}{{ isBinary ? (customLegendValue | dimlessBinary) : (customLegend[1] | dimless) }}
+ + + + + + + + + +
Total Shards:  {{ total }}
Transferred Shards: {{ used }}
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 -- 2.39.5