Skip to content

Commit 2d37276

Browse files
committed
mgr/dashboard: add hardware status summary
On the landing page of the Dashboard, add the hardware status summary Fixes:https://tracker.ceph.com/issues/64329 Signed-off-by: Pedro Gonzalez Gomez <[email protected]>
1 parent bf3a294 commit 2d37276

File tree

15 files changed

+358
-10
lines changed

15 files changed

+358
-10
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
from typing import List, Optional
3+
4+
from ..services.hardware import HardwareService
5+
from . import APIDoc, APIRouter, EndpointDoc, RESTController
6+
from ._version import APIVersion
7+
8+
9+
@APIRouter('/hardware')
10+
@APIDoc("Hardware management API", "Hardware")
11+
class Hardware(RESTController):
12+
13+
@RESTController.Collection('GET', version=APIVersion.EXPERIMENTAL)
14+
@EndpointDoc("Retrieve a summary of the hardware health status")
15+
def summary(self, categories: Optional[List[str]] = None, hostname: Optional[List[str]] = None):
16+
"""
17+
Get the health status of as many hardware categories, or all of them if none is given
18+
:param categories: The hardware type, all of them by default
19+
:param hostname: The host to retrieve from, all of them by default
20+
"""
21+
return HardwareService.get_summary(categories, hostname)

src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@
5656
link="/hosts"
5757
title="Host"
5858
summaryType="simplified"
59-
*ngIf="healthData.hosts != null"></cd-card-row>
59+
*ngIf="healthData.hosts != null"
60+
[dropdownData]="(isHardwareEnabled$ | async) && (hardwareSummary$ | async)">
61+
</cd-card-row>
6062
<!-- Monitors -->
6163
<cd-card-row [data]="healthData.mon_status.monmap.mons.length"
6264
link="/monitor"
@@ -141,7 +143,7 @@
141143
</ul>
142144
</ng-template>
143145

144-
<div class="d-flex flex-row">
146+
<div class="d-flex flex-row col-md-3 ms-4">
145147
<i *ngIf="healthData.health?.status"
146148
[ngClass]="[healthData.health.status | healthIcon, icons.large2x]"
147149
[ngStyle]="healthData.health.status | healthColor"
@@ -159,6 +161,16 @@
159161
i18n>Cluster</span>
160162
</div>
161163
</div>
164+
165+
<div class="d-flex flex-column col-md-3">
166+
<div *ngIf="hasHardwareError"
167+
class="d-flex flex-row">
168+
<i class="text-danger"
169+
[ngClass]="[icons.danger, icons.large2x]"></i>
170+
<span class="ms-2 mt-n1 lead"
171+
i18n>Hardware</span>
172+
</div>
173+
</div>
162174
<section class="footer alerts"
163175
*ngIf="isAlertmanagerConfigured && prometheusAlertService.alerts.length">
164176
<div class="d-flex flex-wrap ms-4 me-4 mb-3 mt-3">

src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { Component, OnDestroy, OnInit } from '@angular/core';
22

33
import _ from 'lodash';
4-
import { Observable, Subscription } from 'rxjs';
5-
import { take } from 'rxjs/operators';
4+
import { BehaviorSubject, Observable, Subscription, of } from 'rxjs';
5+
import { switchMap, take } from 'rxjs/operators';
66

77
import { HealthService } from '~/app/shared/api/health.service';
88
import { OsdService } from '~/app/shared/api/osd.service';
@@ -24,6 +24,7 @@ import { PrometheusAlertService } from '~/app/shared/services/prometheus-alert.s
2424
import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
2525
import { MgrModuleService } from '~/app/shared/api/mgr-module.service';
2626
import { AlertClass } from '~/app/shared/enum/health-icon.enum';
27+
import { HardwareService } from '~/app/shared/api/hardware.service';
2728

2829
@Component({
2930
selector: 'cd-dashboard-v3',
@@ -69,6 +70,12 @@ export class DashboardV3Component extends PrometheusListHelper implements OnInit
6970
telemetryEnabled: boolean;
7071
telemetryURL = 'https://telemetry-public.ceph.com/';
7172
origin = window.location.origin;
73+
hardwareHealth: any;
74+
hardwareEnabled: boolean = false;
75+
hasHardwareError: boolean = false;
76+
isHardwareEnabled$: Observable<boolean>;
77+
hardwareSummary$: Observable<any>;
78+
hardwareSubject = new BehaviorSubject<any>([]);
7279

7380
constructor(
7481
private summaryService: SummaryService,
@@ -80,7 +87,8 @@ export class DashboardV3Component extends PrometheusListHelper implements OnInit
8087
public prometheusService: PrometheusService,
8188
private mgrModuleService: MgrModuleService,
8289
private refreshIntervalService: RefreshIntervalService,
83-
public prometheusAlertService: PrometheusAlertService
90+
public prometheusAlertService: PrometheusAlertService,
91+
private hardwareService: HardwareService
8492
) {
8593
super(prometheusService);
8694
this.permissions = this.authStorageService.getPermissions();
@@ -89,9 +97,21 @@ export class DashboardV3Component extends PrometheusListHelper implements OnInit
8997

9098
ngOnInit() {
9199
super.ngOnInit();
100+
this.isHardwareEnabled$ = this.getHardwareConfig();
101+
this.hardwareSummary$ = this.hardwareSubject.pipe(
102+
switchMap(() =>
103+
this.hardwareService.getSummary().pipe(
104+
switchMap((data: any) => {
105+
this.hasHardwareError = data.host.flawed;
106+
return of(data);
107+
})
108+
)
109+
)
110+
);
92111
this.interval = this.refreshIntervalService.intervalData$.subscribe(() => {
93112
this.getHealth();
94113
this.getCapacityCardData();
114+
if (this.hardwareEnabled) this.hardwareSubject.next([]);
95115
});
96116
this.getPrometheusData(this.prometheusService.lastHourDateObject);
97117
this.getDetailsCardData();
@@ -163,4 +183,13 @@ export class DashboardV3Component extends PrometheusListHelper implements OnInit
163183
trackByFn(index: any) {
164184
return index;
165185
}
186+
187+
getHardwareConfig(): Observable<any> {
188+
return this.mgrModuleService.getConfig('cephadm').pipe(
189+
switchMap((resp: any) => {
190+
this.hardwareEnabled = resp?.hw_monitoring;
191+
return of(resp?.hw_monitoring);
192+
})
193+
);
194+
}
166195
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { HttpClientTestingModule } from '@angular/common/http/testing';
2+
import { TestBed } from '@angular/core/testing';
3+
4+
import { configureTestBed } from '~/testing/unit-test-helper';
5+
import { HardwareService } from './hardware.service';
6+
7+
describe('HardwareService', () => {
8+
let service: HardwareService;
9+
10+
configureTestBed({
11+
providers: [HardwareService],
12+
imports: [HttpClientTestingModule]
13+
});
14+
15+
beforeEach(() => {
16+
TestBed.configureTestingModule({});
17+
service = TestBed.inject(HardwareService);
18+
});
19+
20+
it('should be created', () => {
21+
expect(service).toBeTruthy();
22+
});
23+
});
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { HttpClient } from '@angular/common/http';
2+
import { Injectable } from '@angular/core';
3+
4+
@Injectable({
5+
providedIn: 'root'
6+
})
7+
export class HardwareService {
8+
baseURL = 'api/hardware';
9+
10+
constructor(private http: HttpClient) {}
11+
12+
getSummary(category: string[] = []): any {
13+
return this.http.get<any>(`${this.baseURL}/summary`, {
14+
params: { categories: category },
15+
headers: { Accept: 'application/vnd.ceph.api.v0.1+json' }
16+
});
17+
}
18+
}

src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.html

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<hr>
22
<li class="list-group-item">
3-
<div class="d-flex pl-1 pb-2 pt-2">
3+
<div class="d-flex pl-1 pb-2 pt-2 position-relative">
44
<div class="ms-4 me-auto">
55
<a [routerLink]="link"
66
*ngIf="link && total > 0; else noLinkTitle"
@@ -12,7 +12,7 @@
1212
<ng-template ngPluralCase="other">{{ title }}s</ng-template>
1313
</a>
1414
</div>
15-
<span class="me-3">
15+
<span class="me-4">
1616
<ng-container [ngSwitch]="summaryType">
1717
<ng-container *ngSwitchCase="'iscsi'">
1818
<ng-container *ngTemplateOutlet="iscsiSummary"></ng-container>
@@ -28,9 +28,22 @@
2828
</ng-container>
2929
</ng-container>
3030
</span>
31+
<span *ngIf="dropdownData && dropdownData.total.total.total > 0"
32+
class="position-absolute end-0 me-2">
33+
<a (click)="toggleDropdown()"
34+
class="dropdown-toggle"
35+
[attr.aria-expanded]="dropdownToggled"
36+
aria-controls="row-dropdwon"
37+
role="button"></a>
38+
</span>
3139
</div>
3240
</li>
3341

42+
<div *ngIf="dropdownToggled">
43+
<hr>
44+
<ng-container *ngTemplateOutlet="dropdownTemplate"></ng-container>
45+
</div>
46+
3447
<ng-template #defaultSummary>
3548
<span *ngIf="data.success || data.categoryPgAmount?.clean || (data.success === 0 && data.total === 0)">
3649
<span *ngIf="data.success || (data.success === 0 && data.total === 0)">
@@ -153,11 +166,23 @@
153166
</ng-template>
154167

155168
<ng-template #simplifiedSummary>
156-
<span>
169+
<span *ngIf="!dropdownTotalError else showErrorNum">
157170
{{ data }}
158171
<i class="text-success"
159172
[ngClass]="[icons.success]"></i>
160173
</span>
174+
<ng-template #showErrorNum>
175+
<span *ngIf="data - dropdownTotalError > 0">
176+
{{ data - dropdownTotalError }}
177+
<i class="text-success"
178+
[ngClass]="[icons.success]"></i>
179+
</span>
180+
<span>
181+
{{ dropdownTotalError }}
182+
<i class="text-danger"
183+
[ngClass]="[icons.danger]"></i>
184+
</span>
185+
</ng-template>
161186
</ng-template>
162187

163188
<ng-template #noLinkTitle>
@@ -169,3 +194,36 @@
169194
<ng-template ngPluralCase="other">{{ title }}s</ng-template>
170195
</span>
171196
</ng-template>
197+
198+
<ng-template #dropdownTemplate>
199+
<ng-container *ngFor="let data of dropdownData?.total.category | keyvalue">
200+
<li class="list-group-item">
201+
<div class="d-flex pb-2 pt-2">
202+
<div class="ms-5 me-auto">
203+
<span *ngIf="data.value.total"
204+
[ngPlural]="data.value.total"
205+
i18n>
206+
{{ data.value.total }}
207+
<ng-template ngPluralCase="=0">{{ hwNames[data.key] }}</ng-template>
208+
<ng-template ngPluralCase="=1">{{ hwNames[data.key] }}</ng-template>
209+
<ng-template ngPluralCase="other">{{ hwNames[data.key] | pluralize }}</ng-template>
210+
</span>
211+
</div>
212+
<span [ngClass]="data.value.error ? 'me-2' : 'me-4'">
213+
{{ data.value.ok }}
214+
<i class="text-success"
215+
*ngIf="data.value.ok"
216+
[ngClass]="[icons.success]">
217+
</i>
218+
</span>
219+
<span *ngIf="data.value.error"
220+
class="me-4 ms-2">
221+
{{ data.value.error }}
222+
<i class="text-danger"
223+
[ngClass]="[icons.danger]">
224+
</i>
225+
</span>
226+
</div>
227+
</li>
228+
</ng-container>
229+
</ng-template>

src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,18 @@
22
border: 0;
33
font-size: 14px;
44
}
5+
6+
a.dropdown-toggle {
7+
&::after {
8+
border: 0;
9+
content: '\f054';
10+
font-family: 'ForkAwesome';
11+
font-size: 1rem;
12+
margin-top: 0.15rem;
13+
transition: transform 0.3s ease-in-out;
14+
}
15+
16+
&[aria-expanded='true']::after {
17+
transform: rotate(90deg);
18+
}
19+
}

src/pybind/mgr/dashboard/frontend/src/app/shared/components/card-row/card-row.component.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Component, Input, OnChanges } from '@angular/core';
22
import { Icons } from '~/app/shared/enum/icons.enum';
3+
import { HardwareNameMapping } from '~/app/shared/enum/hardware.enum';
34

45
@Component({
56
selector: 'cd-card-row',
@@ -19,8 +20,14 @@ export class CardRowComponent implements OnChanges {
1920
@Input()
2021
summaryType = 'default';
2122

23+
@Input()
24+
dropdownData: any;
25+
26+
hwNames = HardwareNameMapping;
2227
icons = Icons;
2328
total: number;
29+
dropdownTotalError: number = 0;
30+
dropdownToggled: boolean = false;
2431

2532
ngOnChanges(): void {
2633
if (this.data.total || this.data.total === 0) {
@@ -30,5 +37,15 @@ export class CardRowComponent implements OnChanges {
3037
} else {
3138
this.total = this.data;
3239
}
40+
41+
if (this.dropdownData) {
42+
if (this.title == 'Host') {
43+
this.dropdownTotalError = this.dropdownData.host.flawed;
44+
}
45+
}
46+
}
47+
48+
toggleDropdown(): void {
49+
this.dropdownToggled = !this.dropdownToggled;
3350
}
3451
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export enum HardwareNameMapping {
2+
memory = 'Memory',
3+
storage = 'Drive',
4+
processors = 'CPU',
5+
network = 'Network',
6+
power = 'Power supply',
7+
fans = 'Fan module'
8+
}

src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { TruncatePipe } from './truncate.pipe';
3737
import { UpperFirstPipe } from './upper-first.pipe';
3838
import { OctalToHumanReadablePipe } from './octal-to-human-readable.pipe';
3939
import { PathPipe } from './path.pipe';
40+
import { PluralizePipe } from './pluralize.pipe';
4041

4142
@NgModule({
4243
imports: [CommonModule],
@@ -76,7 +77,8 @@ import { PathPipe } from './path.pipe';
7677
MdsSummaryPipe,
7778
OsdSummaryPipe,
7879
OctalToHumanReadablePipe,
79-
PathPipe
80+
PathPipe,
81+
PluralizePipe
8082
],
8183
exports: [
8284
ArrayPipe,
@@ -114,7 +116,8 @@ import { PathPipe } from './path.pipe';
114116
MdsSummaryPipe,
115117
OsdSummaryPipe,
116118
OctalToHumanReadablePipe,
117-
PathPipe
119+
PathPipe,
120+
PluralizePipe
118121
],
119122
providers: [
120123
ArrayPipe,

0 commit comments

Comments
 (0)