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")
--- /dev/null
+@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;
+ }
+ }
+ }
+}
<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>
+@use './src/styles/vendor/variables' as vv;
+
hr {
margin-bottom: 2px;
margin-top: 2px;
.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;
+}
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', () => {
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';
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',
styleUrls: ['./rgw-overview-dashboard.component.scss']
})
export class RgwOverviewDashboardComponent implements OnInit, OnDestroy {
+ icons = Icons;
+
interval = new Subscription();
permissions: Permissions;
rgwDaemonCount = 0;
realmData: any;
daemonSub: Subscription;
realmSub: Subscription;
+ multisiteInfo: object[] = [];
ZonegroupSub: Subscription;
ZoneSUb: Subscription;
UserSub: Subscription;
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,
private rgwZoneService: RgwZoneService,
private rgwBucketService: RgwBucketService,
private rgwUserService: RgwUserService,
- private prometheusService: PrometheusService
+ private prometheusService: PrometheusService,
+ private rgwMultisiteService: RgwMultisiteService
) {
this.permissions = this.authStorageService.getPermissions();
}
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;
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() {
true
);
}
+
+ getSyncStatus() {
+ this.subject.next();
+ }
+
+ trackByFn(zone: any) {
+ return zone;
+ }
}
--- /dev/null
+<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>
--- /dev/null
+@use './src/styles/vendor/variables' as vv;
+
+ul {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ list-style-type: none;
+}
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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() {}
+}
--- /dev/null
+<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>
--- /dev/null
+@use './src/styles/vendor/variables' as vv;
+
+ul {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ list-style-type: none;
+}
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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() {}
+}
--- /dev/null
+<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>
--- /dev/null
+@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;
+}
--- /dev/null
+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();
+ });
+});
--- /dev/null
+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() {}
+}
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';
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: [
NgbNavModule,
RouterModule,
NgbTooltipModule,
+ NgbPopoverModule,
NgxPipeFunctionModule,
TreeModule,
DataTableModule,
RgwMultisiteImportComponent,
RgwMultisiteExportComponent,
CreateRgwServiceEntitiesComponent,
- RgwOverviewDashboardComponent
+ RgwOverviewDashboardComponent,
+ RgwSyncPrimaryZoneComponent,
+ RgwSyncMetadataInfoComponent,
+ RgwSyncDataInfoComponent
]
})
export class RgwModule {}
<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>
overflow: auto;
position: absolute;
}
+
+.rgw-dashboard {
+ background-color: vv.$body-bg-alt;
+}
return this.http.put(`${this.url}/migrate`, null, { params: params });
});
}
+
+ getSyncStatus() {
+ return this.http.get(`${this.url}/sync_status`);
+ }
}
-<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>
import { Component, Input } from '@angular/core';
+import { Icons } from '~/app/shared/enum/icons.enum';
@Component({
selector: 'cd-card',
styleUrls: ['./card.component.scss']
})
export class CardComponent {
+ icons = Icons;
+
@Input()
cardTitle: string;
+ @Input()
+ cardType: string = '';
+ @Input()
+ removeBorder = false;
+ @Input()
+ shadow = false;
}
<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: </td>
+ <td class="text-right"><strong> {{ total }}</strong></td>
+ </tr>
+ <tr *ngIf="calculatePerc">
+ <td class="text-left">Transferred Shards: </td>
+ <td class="'text-right"><strong>{{ used }}</strong></td>
+ </tr>
+ </table>
</ng-template>
<div class="progress"
}
span {
- color: vv.$black;
+ color: vv.$white;
display: block;
font-weight: normal;
position: absolute;
customLegendValue?: string;
@Input()
showFreeToolTip = true;
+ @Input()
+ showMultisiteTooltip = false;
usedPercentage: number;
freePercentage: number;
'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`,
@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';
$chart-danger: #c9190b !default;
$chart-color-strong-blue: #0078c8 !default;
$chart-color-translucent-blue: #0096dc80 !default;
+$chart-color-border: #00000020 !default;
// Typography
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