@let data = data$ | async;
@let hwEnabled = enabled$ | async;
-@let hwSections = sections$ | async;
+@let hwData = hardwareData$ | async;
@let telemetryEnabled = telemetryEnabled$ | async;
@let colorClass = 'overview-health-card-status--' + vm?.clusterHealth?.icon;
class="cds-ml-3"
[caret]="true"
description="Health incidents represent Ceph health check warnings that indicate abnormal conditions requiring intervention and persist until the condition is resolved."
- i8n-description
+ i18n-description
>
<cd-icon type="help"></cd-icon> |
</cds-tooltip>
<cds-skeleton-text [lines]="1"></cds-skeleton-text>
}
<!-- HARDWARE TAB -->
- @if (hwEnabled && hwSections) {
+ @if (hwEnabled && hwData?.sections) {
<div
class="overview-health-card-tab"
[class.overview-health-card-tab-selected]="activeSection === 'hardware'"
>
- <div class="cds-mb-1"><cd-icon [type]="vm?.overallSystemSev"></cd-icon></div>
+ <div class="cds-mb-1"><cd-icon [type]="hwData.overallSeverity"></cd-icon></div>
<cds-tooltip-definition
[highContrast]="true"
[openOnHover]="true"
<!-- HARDWARE TAB CONTENT -->
<ng-container *ngSwitchCase="'hardware'">
<div class="overview-health-card-tab-content">
- <p
- class="overview-health-card-secondary-text cds--type-body-compact-01"
- i18n
- >
- Some cluster components are degraded and may require attention.
- </p>
+ @if (hwData?.overallSeverity && hwData.overallSeverity !== 'success') {
+ <p
+ class="overview-health-card-secondary-text cds--type-body-compact-01"
+ i18n
+ >
+ Some hardware components are degraded and may require attention.
+ </p>
+ } @else {
+ <p
+ class="overview-health-card-secondary-text cds--type-body-compact-01"
+ i18n
+ >
+ All hardware components are operating normally.
+ </p>
+ }
- @if (hwEnabled && hwSections) {
+ @if (hwEnabled && hwData?.sections) {
<div class="overview-health-card-hardware-sections">
- @for (section of hwSections; track $index; let isLast = $last) {
- <div class="overview-health-card-hardware-section"
- [class.border-subtle-right]="!isLast">
- @for (row of section; track row.key) {
- <div class="overview-health-card-hardware-row">
- <span class="overview-health-card-icon-and-text">
- <cd-icon [type]="row.icon"></cd-icon>
- <span class="cds--type-body-compact-01">
- {{ row.label }}
- </span>
- </span>
+ @for (section of hwData.sections; track $index; let isLast = $last) {
+ <div
+ class="overview-health-card-hardware-section"
+ [class.border-subtle-right]="!isLast"
+ >
+ @for (row of section; track row.key) {
+ <div class="overview-health-card-hardware-row">
+ <span class="overview-health-card-icon-and-text">
+ <cd-icon [type]="row.icon"></cd-icon>
+ <span class="cds--type-body-compact-01">
+ {{ row.label }}
+ </span>
+ </span>
<span class="overview-health-card-hardware-status">
- @if (row.error > 0) {
- <cd-icon type="warningAlt"></cd-icon>
+ @for (status of row.statusCounts; track status.icon) {
+ <cd-icon [type]="status.icon"></cd-icon>
<span class="cds--type-body-compact-01">
- {{ row.error }}
+ {{ status.count }}
</span>
}
- <cd-icon type="checkMarkOutline"></cd-icon>
- <span class="cds--type-body-compact-01">
- {{ row.ok }}
- </span>
</span>
</div>
}
&-hardware-sections {
display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
+ grid-template-columns: repeat(4, minmax(0, 1fr));
column-gap: var(--cds-spacing-03);
width: 100%;
margin-top: var(--cds-spacing-03);
const MOCK_HW_SUMMARY = {
total: {
category: {
- memory: { total: 8, ok: 8, error: 0 },
- storage: { total: 4, ok: 3, error: 1 },
- processors: { total: 2, ok: 2, error: 0 },
- network: { total: 6, ok: 5, error: 1 },
- power: { total: 4, ok: 4, error: 0 },
- fans: { total: 12, ok: 10, error: 2 }
+ memory: { total: 8, ok: 8, warn: 0, critical: 0 },
+ storage: { total: 4, ok: 2, warn: 1, critical: 1 },
+ processors: { total: 2, ok: 2, warn: 0, critical: 0 },
+ network: { total: 6, ok: 4, warn: 1, critical: 1 },
+ power: { total: 4, ok: 4, warn: 0, critical: 0 },
+ fans: { total: 12, ok: 8, warn: 2, critical: 2 },
+ temperatures: { total: 3, ok: 3, warn: 0, critical: 0 }
},
- total: { total: 36, ok: 32, error: 4 }
+ total: { total: 39, ok: 31, warn: 4, critical: 4 }
},
host: { flawed: 2 }
};
processors: 'chip',
network: 'network1',
power: 'plug',
- fans: 'ibmStreamSets'
+ fans: 'ibmStreamSets',
+ temperatures: 'temperature'
};
describe('OverviewHealthCardComponent', () => {
expect(component).toBeTruthy();
});
- describe('hardware sections', () => {
- it('should emit hardware rows grouped in pairs', (done) => {
- component.sections$.subscribe((sections) => {
- expect(sections).not.toBeNull();
- sections!.forEach((section) => {
- expect(section.length).toBeLessThanOrEqual(2);
- });
- done();
- });
- });
-
- it('should limit to 3 groups (6 categories max)', (done) => {
- component.sections$.subscribe((sections) => {
- expect(sections).not.toBeNull();
- expect(sections!.length).toBeLessThanOrEqual(3);
+ describe('hardware data', () => {
+ it('should emit 7 hardware rows in 4 sections', (done) => {
+ component.hardwareData$.subscribe((hwData) => {
+ expect(hwData).not.toBeNull();
+ expect(hwData!.sections.length).toBe(4);
+ expect(hwData!.sections.flat().length).toBe(7);
done();
});
});
- it('should map each row to correct icon from HW_ICON_MAP', (done) => {
- component.sections$.subscribe((sections) => {
- const rows = sections!.flat();
+ it('should map each row to correct icon from HARDWARE_ICON_MAP', (done) => {
+ component.hardwareData$.subscribe((hwData) => {
+ const rows = hwData!.sections.flat();
rows.forEach((row) => {
expect(row.icon).toBe(EXPECTED_ICON_MAP[row.key]);
});
});
it('should map each row to correct label from HardwareNameMapping', (done) => {
- component.sections$.subscribe((sections) => {
- const rows = sections!.flat();
+ component.hardwareData$.subscribe((hwData) => {
+ const rows = hwData!.sections.flat();
rows.forEach((row) => {
- expect(row.label).toBe(
- HardwareNameMapping[row.key as keyof typeof HardwareNameMapping]
- );
+ expect(row.label).toBe(HardwareNameMapping[row.key as keyof typeof HardwareNameMapping]);
});
done();
});
});
- it('should compute ok and error counts from summary data', (done) => {
- component.sections$.subscribe((sections) => {
- const rows = sections!.flat();
+ it('should build statusCounts with correct ok/warn/critical counts', (done) => {
+ component.hardwareData$.subscribe((hwData) => {
+ const rows = hwData!.sections.flat();
+
const memoryRow = rows.find((r) => r.key === 'memory');
- expect(memoryRow?.ok).toBe(8);
- expect(memoryRow?.error).toBe(0);
+ expect(memoryRow?.statusCounts).toEqual([{ icon: 'success', count: 8 }]);
const storageRow = rows.find((r) => r.key === 'storage');
- expect(storageRow?.ok).toBe(3);
- expect(storageRow?.error).toBe(1);
+ expect(storageRow?.statusCounts).toEqual([
+ { icon: 'error', count: 1 },
+ { icon: 'warningAltFilled', count: 1 },
+ { icon: 'success', count: 2 }
+ ]);
const fansRow = rows.find((r) => r.key === 'fans');
- expect(fansRow?.ok).toBe(10);
- expect(fansRow?.error).toBe(2);
+ expect(fansRow?.statusCounts).toEqual([
+ { icon: 'error', count: 2 },
+ { icon: 'warningAltFilled', count: 2 },
+ { icon: 'success', count: 8 }
+ ]);
+ done();
+ });
+ });
+
+ it('should compute correct severity per row', (done) => {
+ component.hardwareData$.subscribe((hwData) => {
+ const rows = hwData!.sections.flat();
+
+ const memoryRow = rows.find((r) => r.key === 'memory');
+ expect(memoryRow?.severity).toBe(0);
+
+ const storageRow = rows.find((r) => r.key === 'storage');
+ expect(storageRow?.severity).toBe(2);
+
+ const powerRow = rows.find((r) => r.key === 'power');
+ expect(powerRow?.severity).toBe(0);
done();
});
});
- it('should include exactly memory, storage, processors, network, power, fans', (done) => {
- component.sections$.subscribe((sections) => {
- const keys = sections!.flat().map((r) => r.key);
- expect(keys).toEqual(['memory', 'storage', 'processors', 'network', 'power', 'fans']);
+ it('should compute overall severity as worst across all categories', (done) => {
+ component.hardwareData$.subscribe((hwData) => {
+ expect(hwData!.overallSeverity).toBe('error');
done();
});
});
+
+ it('should include all 7 categories', (done) => {
+ component.hardwareData$.subscribe((hwData) => {
+ const keys = hwData!.sections.flat().map((r) => r.key);
+ expect(keys).toEqual([
+ 'memory',
+ 'storage',
+ 'processors',
+ 'network',
+ 'power',
+ 'fans',
+ 'temperatures'
+ ]);
+ done();
+ });
+ });
+ });
+});
+
+describe('OverviewHealthCardComponent (all healthy)', () => {
+ let component: OverviewHealthCardComponent;
+ let fixture: ComponentFixture<OverviewHealthCardComponent>;
+
+ const healthyMock = {
+ total: {
+ category: {
+ memory: { total: 4, ok: 4, warn: 0, critical: 0 },
+ storage: { total: 2, ok: 2, warn: 0, critical: 0 },
+ processors: { total: 1, ok: 1, warn: 0, critical: 0 },
+ network: { total: 2, ok: 2, warn: 0, critical: 0 },
+ power: { total: 2, ok: 2, warn: 0, critical: 0 },
+ fans: { total: 4, ok: 4, warn: 0, critical: 0 },
+ temperatures: { total: 3, ok: 3, warn: 0, critical: 0 }
+ },
+ total: { total: 18, ok: 18, warn: 0, critical: 0 }
+ },
+ host: { flawed: 0 }
+ };
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ OverviewHealthCardComponent,
+ CommonModule,
+ ProductiveCardComponent,
+ SkeletonModule,
+ ButtonModule,
+ RouterModule,
+ ComponentsModule,
+ LinkModule,
+ PipesModule
+ ],
+ providers: [
+ {
+ provide: SummaryService,
+ useValue: { summaryData$: of({ version: 'ceph version 18.0.0 reef (dev)' }) }
+ },
+ { provide: UpgradeService, useValue: { listCached: jest.fn(() => of(null)) } },
+ {
+ provide: AuthStorageService,
+ useValue: { getPermissions: jest.fn(() => ({ configOpt: { read: true } })) }
+ },
+ {
+ provide: MgrModuleService,
+ useValue: { getConfig: jest.fn(() => of({ hw_monitoring: true })) }
+ },
+ { provide: HardwareService, useValue: { getSummary: jest.fn(() => of(healthyMock)) } },
+ { provide: HealthService, useValue: { getTelemetryStatus: jest.fn(() => of(false)) } },
+ provideRouter([])
+ ]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(OverviewHealthCardComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should report overall severity as success when all healthy', (done) => {
+ component.hardwareData$.subscribe((hwData) => {
+ expect(hwData!.overallSeverity).toBe('success');
+ done();
+ });
+ });
+
+ it('should have only ok statusCounts per row', (done) => {
+ component.hardwareData$.subscribe((hwData) => {
+ hwData!.sections.flat().forEach((row) => {
+ expect(row.statusCounts.length).toBe(1);
+ expect(row.statusCounts[0].icon).toBe('success');
+ });
+ done();
+ });
+ });
+});
+
+describe('OverviewHealthCardComponent (warn only)', () => {
+ let component: OverviewHealthCardComponent;
+ let fixture: ComponentFixture<OverviewHealthCardComponent>;
+
+ const warnMock = {
+ total: {
+ category: {
+ memory: { total: 4, ok: 3, warn: 1, critical: 0 },
+ storage: { total: 2, ok: 2, warn: 0, critical: 0 },
+ processors: { total: 1, ok: 1, warn: 0, critical: 0 },
+ network: { total: 2, ok: 2, warn: 0, critical: 0 },
+ power: { total: 2, ok: 2, warn: 0, critical: 0 },
+ fans: { total: 4, ok: 4, warn: 0, critical: 0 },
+ temperatures: { total: 3, ok: 3, warn: 0, critical: 0 }
+ },
+ total: { total: 18, ok: 17, warn: 1, critical: 0 }
+ },
+ host: { flawed: 1 }
+ };
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [
+ OverviewHealthCardComponent,
+ CommonModule,
+ ProductiveCardComponent,
+ SkeletonModule,
+ ButtonModule,
+ RouterModule,
+ ComponentsModule,
+ LinkModule,
+ PipesModule
+ ],
+ providers: [
+ {
+ provide: SummaryService,
+ useValue: { summaryData$: of({ version: 'ceph version 18.0.0 reef (dev)' }) }
+ },
+ { provide: UpgradeService, useValue: { listCached: jest.fn(() => of(null)) } },
+ {
+ provide: AuthStorageService,
+ useValue: { getPermissions: jest.fn(() => ({ configOpt: { read: true } })) }
+ },
+ {
+ provide: MgrModuleService,
+ useValue: { getConfig: jest.fn(() => of({ hw_monitoring: true })) }
+ },
+ { provide: HardwareService, useValue: { getSummary: jest.fn(() => of(warnMock)) } },
+ { provide: HealthService, useValue: { getTelemetryStatus: jest.fn(() => of(false)) } },
+ provideRouter([])
+ ]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(OverviewHealthCardComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should report overall severity as warningAltFilled when only warnings', (done) => {
+ component.hardwareData$.subscribe((hwData) => {
+ expect(hwData!.overallSeverity).toBe('warningAltFilled');
+ done();
+ });
+ });
+
+ it('should include warn statusCount for memory row', (done) => {
+ component.hardwareData$.subscribe((hwData) => {
+ const memoryRow = hwData!.sections.flat().find((r) => r.key === 'memory');
+ expect(memoryRow?.statusCounts).toEqual([
+ { icon: 'warningAltFilled', count: 1 },
+ { icon: 'success', count: 3 }
+ ]);
+ done();
+ });
});
});
providers: [
{
provide: SummaryService,
- useValue: {
- summaryData$: of({
- version: 'ceph version 18.0.0 reef (dev)'
- })
- }
+ useValue: { summaryData$: of({ version: 'ceph version 18.0.0 reef (dev)' }) }
},
{ provide: UpgradeService, useValue: { listCached: jest.fn(() => of(null)) } },
{
provide: AuthStorageService,
useValue: { getPermissions: jest.fn(() => ({ configOpt: { read: true } })) }
},
- { provide: MgrModuleService, useValue: { getConfig: jest.fn(() => of({ hw_monitoring: false })) } },
+ {
+ provide: MgrModuleService,
+ useValue: { getConfig: jest.fn(() => of({ hw_monitoring: false })) }
+ },
{ provide: HardwareService, useValue: { getSummary: jest.fn(() => of(null)) } },
{ provide: HealthService, useValue: { getTelemetryStatus: jest.fn(() => of(false)) } },
provideRouter([])
fixture.detectChanges();
});
- it('should emit null sections when hw monitoring is disabled', (done) => {
- component.sections$.subscribe((sections) => {
- expect(sections).toBeNull();
+ it('should emit null hardwareData when hw monitoring is disabled', (done) => {
+ component.hardwareData$.subscribe((hwData) => {
+ expect(hwData).toBeNull();
done();
});
});
providers: [
{
provide: SummaryService,
- useValue: {
- summaryData$: of({
- version: 'ceph version 18.0.0 reef (dev)'
- })
- }
+ useValue: { summaryData$: of({ version: 'ceph version 18.0.0 reef (dev)' }) }
},
{ provide: UpgradeService, useValue: { listCached: jest.fn(() => of(null)) } },
{
import { UpgradeInfoInterface } from '~/app/shared/models/upgrade.interface';
import { UpgradeService } from '~/app/shared/api/upgrade.service';
import { catchError, filter, map, shareReplay, startWith, switchMap } from 'rxjs/operators';
-import { HealthCardTabSection, HealthCardVM } from '~/app/shared/models/overview';
+import {
+ HealthCardTabSection,
+ HealthCardVM,
+ HardwareCardVM,
+ buildHardwareCardVM
+} from '~/app/shared/models/overview';
import { HardwareService } from '~/app/shared/api/hardware.service';
import { HealthService } from '~/app/shared/api/health.service';
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 = {
i18n?: boolean;
}
-type HwKey = keyof typeof HardwareNameMapping;
-
-type HwRowVM = {
- key: HwKey;
- label: string;
- icon: string;
- ok: number;
- error: number;
-};
-
-const HW_ICON_MAP: Record<HwKey, string> = {
- memory: 'dataEnrichment',
- storage: 'vmdkDisk',
- processors: 'chip',
- network: 'network1',
- power: 'plug',
- fans: 'ibmStreamSets',
-};
-
@Component({
selector: 'cd-overview-health-card',
imports: [
shareReplay({ bufferSize: 1, refCount: true })
);
- private readonly hardwareRows$: Observable<HwRowVM[] | null> = this.hardwareSummary$.pipe(
- map((hw) => {
- const category = hw?.total?.category;
- if (!category) return null;
-
- return (Object.keys(HardwareNameMapping) as HwKey[]).map((key) => ({
- key,
- label: HardwareNameMapping[key],
- icon: HW_ICON_MAP[key],
- ok: Number(category?.[key]?.ok ?? 0),
- error: Number(category?.[key]?.error ?? 0)
- }));
- }),
+ readonly hardwareData$: Observable<HardwareCardVM | null> = this.hardwareSummary$.pipe(
+ map((hw) => buildHardwareCardVM(hw)),
shareReplay({ bufferSize: 1, refCount: true })
);
catchError(() => of(false)),
shareReplay({ bufferSize: 1, refCount: true })
);
-
- readonly sections$: Observable<HwRowVM[][] | null> = this.hardwareRows$.pipe(
- map((rows) => {
- if (!rows) return null;
-
- const result: HwRowVM[][] = [];
- for (let i = 0; i < rows.length; i += 2) {
- result.push(rows.slice(i, i + 2));
- }
- return result.slice(0, 3);
- }),
- shareReplay({ bufferSize: 1, refCount: true })
- );
}
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { HardwareSummary } from '../enum/hardware.enum';
@Injectable({
providedIn: 'root'
constructor(private http: HttpClient) {}
- getSummary(category: string[] = []): any {
- return this.http.get<any>(`${this.baseURL}/summary`, {
+ getSummary(category: string[] = []): Observable<HardwareSummary> {
+ return this.http.get<HardwareSummary>(`${this.baseURL}/summary`, {
params: { categories: category },
headers: { Accept: 'application/vnd.ceph.api.v0.1+json' }
});
import WebServicesCluster20 from '@carbon/icons/es/web-services--cluster/20';
import WebServicesCluster32 from '@carbon/icons/es/web-services--cluster/32';
import CloudMonitoring16 from '@carbon/icons/es/cloud--monitoring/16';
+import Temperature16 from '@carbon/icons/es/temperature/16';
import { TearsheetStepComponent } from './tearsheet-step/tearsheet-step.component';
import { PageHeaderComponent } from './page-header/page-header.component';
Locked16,
WebServicesCluster20,
WebServicesCluster32,
- CloudMonitoring16
+ CloudMonitoring16,
+ Temperature16
]);
}
}
fans = 'Fan module',
temperatures = 'Temperature'
}
+
+export interface HardwareHealthCount {
+ total: number;
+ ok: number;
+ warn: number;
+ critical: number;
+}
+
+export interface HardwareSummary {
+ total: {
+ category: Record<string, HardwareHealthCount>;
+ total: HardwareHealthCount;
+ };
+ host: Record<string, { flawed: boolean } | number>;
+}
inProgress = 'in-progress',
arrowDown = 'arrow--down',
locked = 'locked', // Access denied, locked state
- cloudMonitoring = 'cloud--monitoring'
+ cloudMonitoring = 'cloud--monitoring',
+ temperature = 'temperature'
}
export enum IconSize {
rightArrow: 'caret--right',
locked: 'locked',
cloudMonitoring: 'cloud--monitoring',
- trash: 'trash-can'
+ trash: 'trash-can',
+ temperature: 'temperature'
} as const;
export const EMPTY_STATE_IMAGE = {
import { ChartTabularData, GaugeChartOptions } from '@carbon/charts-angular';
import { HealthCheck, HealthSnapshotMap, PgStateCount } from './health.interface';
+import { HardwareNameMapping, HardwareSummary } from '../enum/hardware.enum';
import _ from 'lodash';
// Types
severity: string;
}
+export interface HardwareStatusCount {
+ icon: string;
+ count: number;
+}
+
+export interface HardwareRowVM {
+ key: string;
+ label: string;
+ icon: string;
+ severity: Severity;
+ statusCounts: HardwareStatusCount[];
+}
+
+export interface HardwareCardVM {
+ sections: HardwareRowVM[][];
+ overallSeverity: string;
+}
+
export interface HealthCardVM {
fsid: string;
overallSystemSev: string;
}
};
}
+
+const HARDWARE_ICON_MAP: Record<string, string> = {
+ memory: 'dataEnrichment',
+ storage: 'vmdkDisk',
+ processors: 'chip',
+ network: 'network1',
+ power: 'plug',
+ fans: 'ibmStreamSets',
+ temperatures: 'temperature'
+};
+
+/**
+ * Mapper: Hardware summary -> HardwareCardVM
+ */
+export function buildHardwareCardVM(hwSummary: HardwareSummary): HardwareCardVM | null {
+ const category = hwSummary?.total?.category;
+ if (!category) return null;
+
+ const severities: Severity[] = [];
+
+ const rows = (Object.keys(HardwareNameMapping) as Array<keyof typeof HardwareNameMapping>).map(
+ (key) => {
+ const ok = Number(category?.[key]?.ok ?? 0);
+ const warn = Number(category?.[key]?.warn ?? 0);
+ const critical = Number(category?.[key]?.critical ?? 0);
+
+ const severity: Severity =
+ critical > 0 ? SEVERITY.err : warn > 0 ? SEVERITY.warn : SEVERITY.ok;
+
+ severities.push(severity);
+
+ const statusCounts: HardwareStatusCount[] = [];
+ if (critical > 0) {
+ statusCounts.push({ icon: SeverityIconMap[SEVERITY.err], count: critical });
+ }
+ if (warn > 0) statusCounts.push({ icon: SeverityIconMap[SEVERITY.warn], count: warn });
+ if (ok > 0) statusCounts.push({ icon: SeverityIconMap[SEVERITY.ok], count: ok });
+
+ return {
+ key,
+ label: HardwareNameMapping[key],
+ icon: HARDWARE_ICON_MAP[key],
+ severity,
+ statusCounts
+ };
+ }
+ );
+
+ const sections = _.chunk(rows, 2);
+ const overallSeverity = maxSeverity(...severities);
+
+ return {
+ sections,
+ overallSeverity: SeverityIconMap[overallSeverity]
+ };
+}
from ..services.orchestrator import OrchClient
STATUS_OK = 'OK'
+STATUS_WARNING = 'Warning'
+STATUS_CRITICAL = 'Critical'
STATUS_UNKNOWN = 'Unknown'
@staticmethod
def get_summary(categories: Optional[List[str]] = None,
hostname: Optional[List[str]] = None):
- total_count = {'total': 0, 'ok': 0, 'error': 0}
+ total_count = {'total': 0, 'ok': 0, 'warn': 0, 'critical': 0}
output: Dict[str, Any] = {
'total': {
return status_val
return STATUS_UNKNOWN
- def count_ok(data: dict) -> int:
- return sum(
- _get_health(component) == STATUS_OK
- for node in data.values()
- for system in node.values()
- for component in system.values()
- )
+ def count_by_health(data: dict) -> Dict[str, int]:
+ counts = {'ok': 0, 'warn': 0, 'critical': 0}
+ for node in data.values():
+ for system in node.values():
+ for component in system.values():
+ health = _get_health(component)
+ if health == STATUS_OK:
+ counts['ok'] += 1
+ elif health == STATUS_WARNING:
+ counts['warn'] += 1
+ elif health == STATUS_CRITICAL:
+ counts['critical'] += 1
+ return counts
def count_total(data: dict) -> int:
return sum(
orch_hardware_instance = OrchClient.instance().hardware
for category in categories:
data = orch_hardware_instance.common(category, hostname)
+ health_counts = count_by_health(data)
category_total = {
'total': count_total(data),
- 'ok': count_ok(data),
- 'error': 0
+ 'ok': health_counts['ok'],
+ 'warn': health_counts['warn'],
+ 'critical': health_counts['critical']
}
for host, systems in data.items():
if not output['host'][host]['flawed']:
for system in systems.values():
if any(_get_health(comp) != STATUS_OK
- for comp in system.values()):
+ for comp in system.values()):
output['host'][host]['flawed'] = True
break
- category_total['error'] = max(0, category_total['total'] - category_total['ok'])
output['total']['category'].setdefault(category, {})
output['total']['category'][category] = category_total
total_count['total'] += category_total['total']
total_count['ok'] += category_total['ok']
- total_count['error'] += category_total['error']
+ total_count['warn'] += category_total['warn']
+ total_count['critical'] += category_total['critical']
output['total']['total'] = total_count
- output['host']['flawed'] = sum(1 for host in output['host']
- if host != 'flawed' and output['host'][host]['flawed'])
+ output['host']['flawed'] = sum(
+ 1 for host in output['host']
+ if host != 'flawed' and output['host'][host]['flawed']
+ )
return output
raise DashboardException(msg=f'{categories} is not a list',
component='Hardware')
if not all(item in categories_list for item in categories):
- raise DashboardException(msg=f'Invalid category, there is no {categories}',
- component='Hardware')
+ raise DashboardException(
+ msg=f'Invalid category, there is no {categories}',
+ component='Hardware'
+ )
return categories
from unittest import mock
from ..exceptions import DashboardException
-from ..services.hardware import HardwareService, STATUS_OK, STATUS_UNKNOWN
-
+from ..services.hardware import STATUS_CRITICAL, STATUS_OK, STATUS_UNKNOWN, \
+ STATUS_WARNING, HardwareService
MOCK_HARDWARE_DATA = {
'memory': {
}
}
+MOCK_WARNING_STATUS_DATA = {
+ 'host1': {
+ 'SystemBoard': {
+ 'DIMM.Socket.A1': {
+ 'description': 'DIMM DDR5',
+ 'status': {'health': 'OK'}
+ },
+ 'DIMM.Socket.A2': {
+ 'description': 'DIMM DDR5',
+ 'status': {'health': 'Warning'}
+ },
+ 'DIMM.Socket.A3': {
+ 'description': 'DIMM DDR5',
+ 'status': {'health': 'Critical'}
+ }
+ }
+ }
+}
+
class HardwareConstantsTest(unittest.TestCase):
def test_status_ok_value(self):
self.assertEqual(STATUS_OK, 'OK')
+ def test_status_warning_value(self):
+ self.assertEqual(STATUS_WARNING, 'Warning')
+
+ def test_status_critical_value(self):
+ self.assertEqual(STATUS_CRITICAL, 'Critical')
+
def test_status_unknown_value(self):
self.assertEqual(STATUS_UNKNOWN, 'Unknown')
class HardwareGetSummaryTest(unittest.TestCase):
def _mock_common(self, data_map):
- def side_effect(category, hostname=None):
+ def side_effect(category, _hostname=None):
return data_map.get(category, {})
return side_effect
cat = result['total']['category']['memory']
self.assertEqual(cat['total'], 3)
self.assertEqual(cat['ok'], 3)
- self.assertEqual(cat['error'], 0)
+ self.assertEqual(cat['warn'], 0)
+ self.assertEqual(cat['critical'], 0)
@mock.patch('dashboard.services.hardware.OrchClient.instance')
def test_dict_status_health_mixed(self, mock_instance):
cat = result['total']['category']['storage']
self.assertEqual(cat['total'], 2)
self.assertEqual(cat['ok'], 1)
- self.assertEqual(cat['error'], 1)
+ self.assertEqual(cat['warn'], 0)
+ self.assertEqual(cat['critical'], 1)
@mock.patch('dashboard.services.hardware.OrchClient.instance')
def test_string_status_format(self, mock_instance):
result = HardwareService.get_summary(categories=['memory'])
cat = result['total']['category']['memory']
self.assertEqual(cat['ok'], 1)
- self.assertEqual(cat['error'], 1)
+ self.assertEqual(cat['warn'], 0)
+ self.assertEqual(cat['critical'], 1)
@mock.patch('dashboard.services.hardware.OrchClient.instance')
- def test_missing_status_treated_as_unknown(self, mock_instance):
+ def test_missing_status_not_counted_as_critical(self, mock_instance):
fake_client = mock.Mock()
fake_client.hardware.common = self._mock_common(
{'memory': MOCK_MISSING_STATUS_DATA}
result = HardwareService.get_summary(categories=['memory'])
cat = result['total']['category']['memory']
self.assertEqual(cat['ok'], 0)
- self.assertEqual(cat['error'], 1)
+ self.assertEqual(cat['warn'], 0)
+ self.assertEqual(cat['critical'], 0)
@mock.patch('dashboard.services.hardware.OrchClient.instance')
- def test_non_dict_component_treated_as_unknown(self, mock_instance):
+ def test_non_dict_component_not_counted_as_critical(self, mock_instance):
fake_client = mock.Mock()
fake_client.hardware.common = self._mock_common(
{'memory': MOCK_NON_DICT_COMPONENT}
result = HardwareService.get_summary(categories=['memory'])
cat = result['total']['category']['memory']
self.assertEqual(cat['ok'], 0)
- self.assertEqual(cat['error'], 1)
+ self.assertEqual(cat['warn'], 0)
+ self.assertEqual(cat['critical'], 0)
@mock.patch('dashboard.services.hardware.OrchClient.instance')
def test_flawed_host_detected(self, mock_instance):
totals = result['total']['total']
self.assertEqual(totals['total'], 9)
self.assertEqual(totals['ok'], 8)
- self.assertEqual(totals['error'], 1)
+ self.assertEqual(totals['warn'], 0)
+ self.assertEqual(totals['critical'], 1)
@mock.patch('dashboard.services.hardware.OrchClient.instance')
def test_empty_data_returns_zeros(self, mock_instance):
cat = result['total']['category']['memory']
self.assertEqual(cat['total'], 0)
self.assertEqual(cat['ok'], 0)
- self.assertEqual(cat['error'], 0)
+ self.assertEqual(cat['warn'], 0)
+ self.assertEqual(cat['critical'], 0)
+
+ @mock.patch('dashboard.services.hardware.OrchClient.instance')
+ def test_warning_status_counted_separately(self, mock_instance):
+ fake_client = mock.Mock()
+ fake_client.hardware.common = self._mock_common(
+ {'memory': MOCK_WARNING_STATUS_DATA}
+ )
+ mock_instance.return_value = fake_client
+
+ result = HardwareService.get_summary(categories=['memory'])
+ cat = result['total']['category']['memory']
+ self.assertEqual(cat['total'], 3)
+ self.assertEqual(cat['ok'], 1)
+ self.assertEqual(cat['warn'], 1)
+ self.assertEqual(cat['critical'], 1)