From: Afreen Misbah Date: Tue, 3 Mar 2026 16:45:48 +0000 (+0530) Subject: mgr/dashboard: Fix snapshot Api firing twice X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=4578aa4393bf71bc7b288af399f1a4115e380484;p=ceph.git mgr/dashboard: Fix snapshot Api firing twice - two subs being created Signed-off-by: Afreen Misbah --- diff --git a/src/pybind/mgr/dashboard/controllers/health.py b/src/pybind/mgr/dashboard/controllers/health.py index 5a84ee921fe..b9509511123 100644 --- a/src/pybind/mgr/dashboard/controllers/health.py +++ b/src/pybind/mgr/dashboard/controllers/health.py @@ -144,6 +144,7 @@ HEALTH_SNAPSHOT_SCHEMA = ({ 'num_pgs': (int, 'Total PG count'), 'bytes_used': (int, 'Used capacity in bytes'), 'bytes_total': (int, 'Total capacity in bytes'), + 'recovering_bytes_per_sec': (int, 'Total recovery in bytes'), }, 'Placement group map details'), 'mgrmap': ({ 'num_active': (int, 'Number of active managers'), @@ -407,6 +408,8 @@ class Health(BaseController): 'read_bytes_sec': data.get('pgmap', {}).get('read_bytes_sec'), 'bytes_used': data.get('pgmap', {}).get('bytes_used'), 'bytes_total': data.get('pgmap', {}).get('bytes_total'), + 'num_pgs': data.get('pgmap', {}).get('num_pgs'), + 'recovering_bytes_per_sec': data.get('pgmap', {}).get('recovering_bytes_per_sec'), } if self._has_permissions(Permission.READ, Scope.MANAGER): @@ -457,7 +460,7 @@ class Health(BaseController): summary['num_hosts'] = len(hosts) available_hosts = [ h for h in hosts - if h.get("status") == "Available" + if h.get("status") == "" ] summary['num_hosts_available'] = len(available_hosts) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts index f8e1f5f44b7..9a9ede1362b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts @@ -83,7 +83,8 @@ describe('Dashbord Component', () => { bytes_total: 325343772672, num_pgs: 497, write_bytes_sec: 0, - read_bytes_sec: 0 + read_bytes_sec: 0, + recovering_bytes_per_sec: 0 }, mgrmap: { num_active: 1, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.html index fca3e2800d9..3b02bf92443 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.html @@ -45,7 +45,7 @@

{{vm?.clusterHealth?.title}} - +

{{vm?.clusterHealth?.message}}

} @else { @@ -257,24 +257,62 @@ -
- - - - {{vm?.resiliencyHealth?.title}} +
+
+ + + + {{vm?.resiliencyHealth?.title}} + - -

- {{vm?.resiliencyHealth?.description}}

- +

+ {{vm?.resiliencyHealth?.description}}

+ +
+ @if (vm?.pgs?.activeCleanChartData && vm?.pgs?.activeCleanChartOptions) { +
+ +
+

Data resiliency reflects data availability and replication (% of placement groups that are active and clean). +

+ @if (vm?.pgs?.activeCleanChartReason?.length) { +

+ {{vm?.pgs?.activeCleanChartSeverity === 'progress' ? 'Data cleanup in progress' : 'What is affecting resiliency?'}} +

+ @for (item of vm?.pgs?.activeCleanChartReason; track item.state; let isLast =$last) { + @if(item.count) { +

+ {{item?.state}}: + {{item.count}} % + @if(!item?.state?.includes('Scrub') || item?.state !== 'remapped') { + + } +

+ } + } + } +
+
+ } @else { + + }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.scss index b8b2a653534..2a436894fcb 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.scss @@ -20,6 +20,10 @@ color: var(--cds-text-secondary); } + &-bold { + font-weight: 700 !important; + } + &-tab { display: flex; } @@ -47,6 +51,20 @@ justify-content: space-between; } + &-resiliency-tab-content { + display: flex; + } + + &-resiliency-chart { + flex-grow: 1; + flex-shrink: 0; + } + + &-resiliency-chart-text { + width: 13rem; + margin-left: var(--cds-spacing-04); + } + &-icon-and-text { display: inline-flex; align-items: center; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts index af9c9871703..7f32ab09227 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/health-card/overview-health-card.component.ts @@ -32,6 +32,7 @@ import { MgrModuleService } from '~/app/shared/api/mgr-module.service'; import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service'; import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; import { HardwareNameMapping } from '~/app/shared/enum/hardware.enum'; +import { GaugeChartComponent } from '@carbon/charts-angular'; type OverviewHealthData = { summary: Summary; @@ -67,7 +68,8 @@ type HwRowVM = { PipesModule, TooltipModule, TabsModule, - LayoutModule + LayoutModule, + GaugeChartComponent ], standalone: true, templateUrl: './overview-health-card.component.html', @@ -88,7 +90,7 @@ export class OverviewHealthCardComponent { @Output() viewPGStates = new EventEmitter(); @Output() activeSectionChange = new EventEmitter(); - activeSection: HealthCardTabSection | null = 'resiliency'; + activeSection: HealthCardTabSection | null; healthItems: HealthItemConfig[] = [ { key: 'mon', label: $localize`Monitor` }, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html index 066a632518a..eee4c1e3730 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.html @@ -86,7 +86,7 @@ class="overview-pg-side-panel-rw-item" [class.overview-pg-side-panel-rw-item--border]="!isLast">

{{data.label}}

-

{{data.value}}

+

{{data.value | dimlessBinary }} / s

} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.scss index 425800f9f11..7ed0a90bfb6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.scss @@ -36,10 +36,6 @@ background-color: var(--cds-background); } - cds-panel .panel-content { - padding: 0 !important; - } - cds-panel .panel-header { margin-bottom: var(--cds-spacing-03); } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts index 82116c2d4ec..574930db6f0 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.spec.ts @@ -91,6 +91,22 @@ describe('OverviewComponent', () => { b: { severity: 'HEALTH_ERR', summary: { message: 'B issue' } } } }, + // data resileincy + pgmap: { + pgs_by_state: [ + { + state_name: 'active+clean', + count: 497 + } + ], + num_pools: 14, + bytes_used: 3236978688, + bytes_total: 325343772672, + num_pgs: 497, + write_bytes_sec: 0, + read_bytes_sec: 0, + recovering_bytes_per_sec: 0 + }, // subsystem inputs used by mapper monmap: { num_mons: 3, quorum: [0, 1, 2] } as any, mgrmap: { num_active: 1, num_standbys: 1 } as any, @@ -157,6 +173,21 @@ describe('OverviewComponent', () => { monmap: { num_mons: 3, quorum: [0, 1, 2] } as any, // ok mgrmap: { num_active: 0, num_standbys: 0 } as any, // err (active < 1) osdmap: { num_osds: 2, up: 2, in: 2 } as any, // ok + pgmap: { + pgs_by_state: [ + { + state_name: 'active+clean', + count: 497 + } + ], + num_pools: 14, + bytes_used: 3236978688, + bytes_total: 325343772672, + num_pgs: 497, + write_bytes_sec: 0, + read_bytes_sec: 0, + recovering_bytes_per_sec: 0 + }, num_hosts: 1, num_hosts_down: 0 // ok } as any; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts index 2a514774bf5..e9f8cecf40f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/overview/overview.component.ts @@ -14,6 +14,8 @@ import { HealthService } from '~/app/shared/api/health.service'; import { RefreshIntervalService } from '~/app/shared/services/refresh-interval.service'; import { HealthCheck, HealthSnapshotMap } from '~/app/shared/models/health.interface'; import { + ACTIVE_CLEAN_CHART_OPTIONS, + calcActiveCleanSeverityAndReasons, getClusterHealth, getHealthChecksAndIncidents, getResiliencyDisplay, @@ -24,6 +26,7 @@ import { safeDifference, SEVERITY, Severity, + SEVERITY_TO_COLOR, SeverityIconMap } from '~/app/shared/models/overview'; @@ -34,16 +37,25 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { OverviewAlertsCardComponent } from './alerts-card/overview-alerts-card.component'; import { PerformanceCardComponent } from '~/app/shared/components/performance-card/performance-card.component'; import { DataTableModule } from '~/app/shared/datatable/datatable.module'; +import { PipesModule } from '~/app/shared/pipes/pipes.module'; /** * Mapper: HealthSnapshotMap -> HealthCardVM * Runs only when healthData$ emits. */ -export function buildHealthCardVM(d: HealthSnapshotMap): HealthCardVM { +function buildHealthCardVM(d: HealthSnapshotMap): HealthCardVM { const checksObj: Record = d.health?.checks ?? {}; const clusterHealth = getClusterHealth(d.health.status as HealthStatus); + const pgStates = d?.pgmap?.pgs_by_state ?? []; + const totalPg = d?.pgmap?.num_pgs ?? 0; + const { incidents, checks } = getHealthChecksAndIncidents(checksObj); - const resiliencyHealth = getResiliencyDisplay(checks); + const resiliencyHealth = getResiliencyDisplay(checks, pgStates); + const { + activeCleanPercent, + severity: activeCleanChartSeverity, + reasons: activeCleanChartReason + } = calcActiveCleanSeverityAndReasons(pgStates, totalPg); // --- System sub-states --- @@ -74,8 +86,6 @@ export function buildHealthCardVM(d: HealthSnapshotMap): HealthCardVM { // Overall = worst of the subsystem severities. const overallSystemSev = maxSeverity(monSev, mgrSev, osdSev, hostsSev); - // Resiliency - return { fsid: d.fsid, overallSystemSev: SeverityIconMap[overallSystemSev], @@ -84,13 +94,19 @@ export function buildHealthCardVM(d: HealthSnapshotMap): HealthCardVM { checks, pgs: { - total: d?.pgmap?.num_pgs, - states: d?.pgmap?.pgs_by_state, + total: totalPg, + states: pgStates, io: [ - { label: $localize`Client write`, value: d?.pgmap?.write_bytes_sec }, - { label: $localize`Client read`, value: d?.pgmap?.read_bytes_sec }, - { label: $localize`Recovery I/O`, value: 0 } - ] + { label: $localize`Client write`, value: d?.pgmap?.write_bytes_sec ?? 0 }, + { label: $localize`Client read`, value: d?.pgmap?.read_bytes_sec ?? 0 }, + { label: $localize`Recovery I/O`, value: d?.pgmap?.recovering_bytes_per_sec ?? 0 } + ], + activeCleanChartData: [{ group: 'value', value: activeCleanPercent }], + activeCleanChartOptions: { + ...ACTIVE_CLEAN_CHART_OPTIONS, + color: { scale: { value: SEVERITY_TO_COLOR[activeCleanChartSeverity] } } + }, + activeCleanChartReason }, clusterHealth, @@ -101,7 +117,7 @@ export function buildHealthCardVM(d: HealthSnapshotMap): HealthCardVM { value: $localize`${mgrActive} active, ${mgrStandby} standby`, severity: SeverityIconMap[mgrSev] }, - osd: { value: $localize`${osdUp}/${osdTotal} in/up`, severity: SeverityIconMap[osdSev] }, + osd: { value: $localize`${osdIn}/${osdUp} in/up`, severity: SeverityIconMap[osdSev] }, hosts: { value: $localize`${hostsAvailable} / ${hostsTotal} available`, severity: SeverityIconMap[hostsSev] @@ -121,7 +137,8 @@ export function buildHealthCardVM(d: HealthSnapshotMap): HealthCardVM { OverviewAlertsCardComponent, PerformanceCardComponent, LayoutModule, - DataTableModule + DataTableModule, + PipesModule ], standalone: true, templateUrl: './overview.component.html', @@ -144,7 +161,7 @@ export class OverviewComponent { private readonly healthData$: Observable = this.refreshIntervalObs(() => this.healthService.getHealthSnapshot() - ); + ).pipe(shareReplay({ bufferSize: 1, refCount: true })); readonly healthCardVm$: Observable = this.healthData$.pipe( map(buildHealthCardVM), diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.ts index ae178ded276..31108019630 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/pg-category.service.ts @@ -52,7 +52,7 @@ export class PgCategoryService { ); } - private getPgStatesFromText(pgStatesText: string) { + getPgStatesFromText(pgStatesText: string) { const pgStates = pgStatesText .replace(/[^a-z_]+/g, ' ') .trim() diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts index b5dd02c4443..913f0f89880 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts @@ -123,6 +123,7 @@ import WarningAlt16 from '@carbon/icons/es/warning--alt/16'; import CheckMarkOutline16 from '@carbon/icons/es/checkmark--outline/16'; import ArrowUpRight16 from '@carbon/icons/es/arrow--up-right/16'; import InProgress16 from '@carbon/icons/es/in-progress/16'; +import ArrowDown16 from '@carbon/icons/es/arrow--down/16'; import { TearsheetStepComponent } from './tearsheet-step/tearsheet-step.component'; import { PageHeaderComponent } from './page-header/page-header.component'; @@ -304,7 +305,8 @@ export class ComponentsModule { WarningAlt16, CheckMarkOutline16, ArrowUpRight16, - InProgress16 + InProgress16, + ArrowDown16 ]); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.scss index f5ad4e445e6..f38d638a62b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/icon/icon.component.scss @@ -62,3 +62,11 @@ Using `color` in css and seyting svg will fill="currentColor does not work. .checkMarkOutline-icon { fill: theme.$support-success !important; } + +.inProgress-icon { + fill: theme.$support-info !important; +} + +.arrowDown-icon { + fill: theme.$support-error !important; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts index eec9d2fc96e..da970125478 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts @@ -124,7 +124,8 @@ export enum Icons { checkMarkOutline = 'checkmark--outline', warningAlt = 'warning--alt', arrowUpRight = 'arrow--up-right', - inProgress = 'in-progress' + inProgress = 'in-progress', + arrowDown = 'arrow--down' } export enum IconSize { @@ -164,5 +165,6 @@ export const ICON_TYPE = { warningAlt: 'warning--alt', checkMarkOutline: 'checkmark--outline', arrowUpRight: ' arrow--up-right', - inProgress: 'in-progress' + inProgress: 'in-progress', + arrowDown: 'arrow--down' } as const; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/health.interface.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/health.interface.ts index adfba20ffc5..d1234047cf1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/health.interface.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/health.interface.ts @@ -40,6 +40,7 @@ export interface PgMap { num_pgs: number; write_bytes_sec: number; read_bytes_sec: number; + recovering_bytes_per_sec: number; } export interface HealthMapCommon { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/overview.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/overview.ts index 23af3a90c56..cb8b40595d8 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/overview.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/overview.ts @@ -1,6 +1,18 @@ +import { ChartTabularData, GaugeChartOptions } from '@carbon/charts-angular'; import { HealthCheck, PgStateCount } from './health.interface'; +import _ from 'lodash'; -export type HealthStatus = 'HEALTH_OK' | 'HEALTH_WARN' | 'HEALTH_ERR'; +// Types +type ResileincyHealthType = { + title: string; + description: string; + icon: string; + severity: ResiliencyState; +}; + +type ResiliencyState = typeof DATA_RESILIENCY_STATE[keyof typeof DATA_RESILIENCY_STATE]; + +type PG_STATES = typeof PG_STATES[number]; export const HealthIconMap = { HEALTH_OK: 'success', @@ -15,8 +27,12 @@ export const SeverityIconMap = { 3: 'inProgress' }; -/** 0 ok, 1 warn, 2 err , 3 sync*/ -export type Severity = 0 | 1 | 2 | 3; +export type HealthStatus = 'HEALTH_OK' | 'HEALTH_WARN' | 'HEALTH_ERR'; + +export type HealthCardTabSection = 'system' | 'hardware' | 'resiliency'; + +/** 0 ok, 1 warn, 2 err */ +export type Severity = 0 | 1 | 2; export type Health = { message: string; @@ -24,25 +40,7 @@ export type Health = { icon: string; }; -const WarnAndErrMessage = $localize`There are active alerts and unresolved health warnings.`; - -export const HealthMap: Record = { - HEALTH_OK: { - message: $localize`All core services are running normally`, - icon: HealthIconMap['HEALTH_OK'], - title: $localize`Healthy` - }, - HEALTH_WARN: { - message: WarnAndErrMessage, - icon: HealthIconMap['HEALTH_WARN'], - title: $localize`Warning` - }, - HEALTH_ERR: { - message: WarnAndErrMessage, - icon: HealthIconMap['HEALTH_ERR'], - title: $localize`Critical` - } -}; +// Interfaces export interface HealthDisplayVM { title: string; @@ -61,12 +59,6 @@ export interface HealthCardSubStateVM { severity: string; } -type ResileincyHealthType = { - title: string; - description: string; - icon: string; -}; - export interface HealthCardVM { fsid: string; overallSystemSev: string; @@ -82,6 +74,9 @@ export interface HealthCardVM { total: number; states: PgStateCount[]; io: Array<{ label: string; value: number }>; + activeCleanChartData: ChartTabularData; + activeCleanChartOptions: GaugeChartOptions; + activeCleanChartReason: Array<{ state: string; count: number }>; }; mon: HealthCardSubStateVM; @@ -90,19 +85,9 @@ export interface HealthCardVM { hosts: HealthCardSubStateVM; } -export type HealthCardTabSection = 'system' | 'hardware' | 'resiliency'; - -export const SEVERITY = { - ok: 0 as Severity, - warn: 1 as Severity, - err: 2 as Severity, - sync: 3 as Severity -} as const; +// Constants -export const RESILIENCY_CHECK = { - error: ['PG_DAMAGED', 'PG_RECOVERY_FULL'], - warn: ['PG_DEGRADED', 'PG_AVAILABILITY', 'PG_BACKFILL_FULL'] -}; +const WarnAndErrMessage = $localize`There are active alerts and unresolved health warnings.`; const DATA_RESILIENCY_STATE = { ok: 'ok', @@ -110,61 +95,133 @@ const DATA_RESILIENCY_STATE = { warn: 'warn', warnDataLoss: 'warnDataLoss', progress: 'progress' +} as const; + +const CHECK_TO_STATE: Record = { + PG_DAMAGED: DATA_RESILIENCY_STATE.error, + PG_RECOVERY_FULL: DATA_RESILIENCY_STATE.error, + + PG_DEGRADED: DATA_RESILIENCY_STATE.warn, + PG_AVAILABILITY: DATA_RESILIENCY_STATE.warnDataLoss, + PG_BACKFILL_FULL: DATA_RESILIENCY_STATE.warn +} as const; + +const RESILIENCY_PRIORITY: Record = { + ok: 0, + progress: 1, + warn: 2, + warnDataLoss: 3, + error: 4 }; -export const DATA_RESILIENCY = { +// Priority: DO NOT CHANGE ORDER HERE +const PG_STATES = [ + // ERROR OR WARN + 'offline', + 'inconsistent', + 'down', + 'stale', + 'degraded', + 'undersized', + 'recovering', + 'recovery_wait', + 'backfilling', + 'backfill_wait', + 'remapped', + // PROGRESS + 'deep', + 'scrubbing' +] as const; + +const LABELS: Record = { + scrubbing: 'Scrub', + deep: 'Deep-Scrub' +}; + +export const HealthMap: Record = { + HEALTH_OK: { + message: $localize`All core services are running normally`, + icon: HealthIconMap['HEALTH_OK'], + title: $localize`Healthy` + }, + HEALTH_WARN: { + message: WarnAndErrMessage, + icon: HealthIconMap['HEALTH_WARN'], + title: $localize`Warning` + }, + HEALTH_ERR: { + message: WarnAndErrMessage, + icon: HealthIconMap['HEALTH_ERR'], + title: $localize`Critical` + } +}; + +export const SEVERITY = { + ok: 0 as Severity, + warn: 1 as Severity, + err: 2 as Severity, + sync: 3 as Severity +} as const; + +export const ACTIVE_CLEAN_CHART_OPTIONS: GaugeChartOptions = { + resizable: true, + height: '100px', + width: '100px', + gauge: { type: 'full' }, + toolbar: { + enabled: false + } +}; + +export const DATA_RESILIENCY: Record = { [DATA_RESILIENCY_STATE.ok]: { icon: 'success', title: $localize`Data is fully replicated and available.`, - description: $localize`All replicas are in place and I/O is operating normally. No action is required.` + description: $localize`All replicas are in place and I/O is operating normally. No action is required.`, + severity: DATA_RESILIENCY_STATE.ok }, [DATA_RESILIENCY_STATE.progress]: { icon: 'inProgress', title: $localize`Data integrity checks in progress`, - description: $localize`Ceph is running routine consistency checks on stored data and metadata to ensure data integrity. Data remains safe and accessible.` + description: $localize`Ceph is running routine consistency checks on stored data and metadata to ensure data integrity. Data remains safe and accessible.`, + severity: DATA_RESILIENCY_STATE.progress }, [DATA_RESILIENCY_STATE.warn]: { icon: 'warning', title: $localize`Restoring data redundancy`, - description: $localize`Some data replicas are missing or not yet in their final location. Ceph is actively rebalancing data to return to a healthy state.` + description: $localize`Some data replicas are missing or not yet in their final location. Ceph is actively rebalancing data to return to a healthy state.`, + severity: DATA_RESILIENCY_STATE.warn }, [DATA_RESILIENCY_STATE.warnDataLoss]: { icon: 'warning', title: $localize`Status unavailable for some data`, - description: $localize`Ceph cannot reliably determine the current state of some data. Availability may be affected.` + description: $localize`Ceph cannot reliably determine the current state of some data. Availability may be affected.`, + severity: DATA_RESILIENCY_STATE.warnDataLoss }, [DATA_RESILIENCY_STATE.error]: { icon: 'error', title: $localize`Data unavailable or inconsistent, manual intervention required`, - description: $localize`Some data is currently unavailable or inconsistent. Ceph could not automatically restore these resources, and manual intervention is required to restore data availability and consistency.` + description: $localize`Some data is currently unavailable or inconsistent. Ceph could not automatically restore these resources, and manual intervention is required to restore data availability and consistency.`, + severity: DATA_RESILIENCY_STATE.error } +} as const; + +export const SEVERITY_TO_COLOR: Record = { + ok: '#24A148', + progress: '#24A148', + warn: '#F1C21B', + warnDataLoss: '#F1C21B', + error: '#DA1E28' }; +// Utilities + export const maxSeverity = (...values: Severity[]): Severity => Math.max(...values) as Severity; export function getClusterHealth(status: HealthStatus): HealthDisplayVM { return HealthMap[status] ?? HealthMap['HEALTH_OK']; } -export function getResiliencyDisplay(checks: HealthCardCheckVM[] = []): ResileincyHealthType { - let resileincyState: string = DATA_RESILIENCY_STATE.ok; - checks.forEach((check) => { - switch (check?.name) { - case RESILIENCY_CHECK.error[0]: - case RESILIENCY_CHECK.error[1]: - resileincyState = DATA_RESILIENCY_STATE.error; - break; - case RESILIENCY_CHECK.warn[0]: - resileincyState = DATA_RESILIENCY_STATE.warn; - break; - case RESILIENCY_CHECK.warn[1]: - resileincyState = DATA_RESILIENCY_STATE.warnDataLoss; - break; - } - }); - return DATA_RESILIENCY[resileincyState]; -} - export function getHealthChecksAndIncidents(checksObj: Record) { const checks: HealthCardCheckVM[] = []; let incidents = 0; @@ -183,3 +240,125 @@ export function getHealthChecksAndIncidents(checksObj: Record RESILIENCY_PRIORITY[state]) state = next; + if (state === DATA_RESILIENCY_STATE.error) break; + } + + if (state === DATA_RESILIENCY_STATE.ok) { + const hasScrubbing = pgStates.some((s) => { + const n = s?.state_name ?? ''; + return n.includes('scrubbing') || n.includes('deep'); + }); + if (hasScrubbing) state = DATA_RESILIENCY_STATE.progress; + } + + return DATA_RESILIENCY[state]; +} + +export function getActiveCleanChartSeverity( + pgStates: PgStateCount[] = [], + activeCleanRatio: number +): ResiliencyState { + if (activeCleanRatio >= 1) return DATA_RESILIENCY_STATE.ok; + + const hasActive = pgStates.some((s) => (s?.state_name ?? '').includes('active')); + return hasActive ? DATA_RESILIENCY_STATE.warn : DATA_RESILIENCY_STATE.error; +} + +function labelOf(key: string) { + return LABELS[key] ?? key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); +} + +function isActiveCleanRow(pgRow: string) { + // E.g active+clean+remapped + return pgRow.includes('active') && pgRow.includes('clean'); +} + +function isScrubbing(pgRow: string) { + return pgRow.includes('scrubbing') || pgRow.includes('deep'); +} + +/** + * If any PG state is active and not clean => Warn + * If any PG state is not active -> Error + * + * In case above is true, the states contributing to that as per + * PG_STATES priotity List will be added. + * + * If all OKAY. then scrubbing shown (if active) + */ +export function calcActiveCleanSeverityAndReasons( + pgStates: PgStateCount[] = [], + totalPg: number +): { + activeCleanPercent: number; + severity: ResiliencyState; + reasons: Array<{ state: string; count: number }>; +} { + if (totalPg <= 0) { + return { activeCleanPercent: 0, severity: DATA_RESILIENCY_STATE.ok, reasons: [] }; + } + + const reasonCounts = new Map(); + let severity: ResiliencyState = DATA_RESILIENCY_STATE.ok; + let activeCleanTotal = 0; + let hasProgress = false; + let hasNotActiveNotClean = false; + let hasActiveNotClean = false; + + for (const state of pgStates) { + const stateName = (state?.state_name ?? '').trim(); + const stateCount = state?.count ?? 0; + const isActive = stateName.includes('active'); + const isClean = stateName.includes('clean'); + + if (!isActive && !isClean) hasNotActiveNotClean = true; + if (isActive && !isClean) hasActiveNotClean = true; + + // If all okay then only scrubbing state is shown + if (!hasProgress && isScrubbing(stateName)) { + hasProgress = true; + } + + // active+clean*: no reasons required hence continuing + if (isActiveCleanRow(stateName)) { + activeCleanTotal += stateCount; + continue; + } + + // Non active, non-clean or non-active+clean: reasons needed + for (const state of PG_STATES) { + if (stateName.includes(state)) { + reasonCounts.set(state, (reasonCounts.get(state) ?? 0) + stateCount); + break; + } + } + } + + if (hasNotActiveNotClean) severity = DATA_RESILIENCY_STATE.error; + else if (hasActiveNotClean) severity = DATA_RESILIENCY_STATE.warn; + else if (hasProgress) severity = DATA_RESILIENCY_STATE.progress; + + const reasons = + reasonCounts.size === 0 + ? [] + : [...reasonCounts.entries()] + .sort((a, b) => b[1] - a[1]) + .map(([state, count]) => ({ + state: labelOf(state), + count: Number(((count / totalPg) * 100).toFixed(2)) + })); + + const activeCleanPercent = Number(((activeCleanTotal / totalPg) * 100).toFixed(2)); + + return { activeCleanPercent, severity, reasons }; +} diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index 8403cb99fea..bce3619b3d2 100755 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -8271,12 +8271,16 @@ paths: - count type: object type: array + recovering_bytes_per_sec: + description: Total recovery in bytes + type: integer required: &id058 - pgs_by_state - num_pools - num_pgs - bytes_used - bytes_total + - recovering_bytes_per_sec type: object required: &id059 - fsid @@ -8434,6 +8438,9 @@ paths: required: *id057 type: object type: array + recovering_bytes_per_sec: + description: Total recovery in bytes + type: integer required: *id058 type: object required: *id059