'scrub_status': (str, '')
})
+HEALTH_SNAPSHOT_SCHEMA = ({
+ 'fsid': (str, 'Cluster filesystem ID'),
+ 'health': ({
+ 'status': (str, 'Overall health status'),
+ 'checks': ({
+ '<check_name>': ({
+ 'severity': (str, 'Health severity level'),
+ 'summary': ({
+ 'message': (str, 'Human-readable summary'),
+ 'count': (int, 'Occurrence count')
+ }, 'Summary details'),
+ 'muted': (bool, 'Whether the check is muted')
+ }, 'Individual health check object')
+ }, 'Health checks keyed by name'),
+ 'mutes': ([str], 'List of muted check names')
+ }, 'Cluster health overview'),
+ 'monmap': ({
+ 'num_mons': (int, 'Number of monitors')
+ }, 'Monitor map details'),
+ 'osdmap': ({
+ 'in': (int, 'Number of OSDs in'),
+ 'up': (int, 'Number of OSDs up'),
+ 'num_osds': (int, 'Total OSD count')
+ }, 'OSD map details'),
+ 'pgmap': ({
+ 'pgs_by_state': ([{
+ 'state_name': (str, 'Placement group state'),
+ 'count': (int, 'Count of PGs in this state')
+ }], 'List of PG counts by state'),
+ 'num_pools': (int, 'Number of pools'),
+ 'num_pgs': (int, 'Total PG count'),
+ 'bytes_used': (int, 'Used capacity in bytes'),
+ 'bytes_total': (int, 'Total capacity in bytes'),
+ }, 'Placement group map details'),
+ 'mgrmap': ({
+ 'num_active': (int, 'Number of active managers'),
+ 'num_standbys': (int, 'Standby manager count')
+ }, 'Manager map details'),
+ 'fsmap': ({
+ 'num_active': (int, 'Number of active mds'),
+ 'num_standbys': (int, 'Standby MDS count'),
+ }, 'Filesystem map details'),
+ 'num_rgw_gateways': (int, 'Count of RGW gateway daemons running'),
+ 'num_iscsi_gateways': ({
+ 'up': (int, 'Count of iSCSI gateways running'),
+ 'down': (int, 'Count of iSCSI gateways not running')
+ }, 'Iscsi gateways status'),
+})
+
class HealthData(object):
"""
class Health(BaseController):
def __init__(self):
super().__init__()
- self.health_full = HealthData(self._has_permissions, minimal=False)
- self.health_minimal = HealthData(self._has_permissions, minimal=True)
+ self._health_full = None
+ self._health_minimal = None
+
+ @property
+ def health_full(self):
+ if self._health_full is None:
+ self._health_full = HealthData(self._has_permissions, minimal=False)
+ return self._health_full
+
+ @property
+ def health_minimal(self):
+ if self._health_minimal is None:
+ self._health_minimal = HealthData(self._has_permissions, minimal=True)
+ return self._health_minimal
@Endpoint()
+ @EndpointDoc("Get Cluster's detailed health report")
def full(self):
return self.health_full.all_health()
@Endpoint()
- @EndpointDoc("Get Cluster's minimal health report",
+ @EndpointDoc("Get Cluster's health report with lesser details",
responses={200: HEALTH_MINIMAL_SCHEMA})
def minimal(self):
return self.health_minimal.all_health()
@Endpoint()
def get_telemetry_status(self):
return mgr.get_module_option_ex('telemetry', 'enabled', False)
+
+ @Endpoint()
+ @EndpointDoc(
+ "Get a quick overview of cluster health at a moment, analogous to "
+ "the ceph status command in CLI.",
+ responses={200: HEALTH_SNAPSHOT_SCHEMA})
+ def snapshot(self):
+ data = CephService.send_command('mon', 'status')
+
+ summary = {
+ 'fsid': data.get('fsid'),
+ 'health': {
+ 'status': data.get('health', {}).get('status'),
+ 'checks': data.get('health', {}).get('checks', {}),
+ 'mutes': data.get('health', {}).get('mutes', []),
+ },
+ }
+
+ if self._has_permissions(Permission.READ, Scope.MONITOR):
+ summary['monmap'] = {
+ 'num_mons': data.get('monmap', {}).get('num_mons'),
+ }
+
+ if self._has_permissions(Permission.READ, Scope.OSD):
+ summary['osdmap'] = {
+ 'in': data.get('osdmap', {}).get('num_in_osds'),
+ 'up': data.get('osdmap', {}).get('num_up_osds'),
+ 'num_osds': data.get('osdmap', {}).get('num_osds'),
+ }
+ summary['pgmap'] = {
+ 'pgs_by_state': data.get('pgmap', {}).get('pgs_by_state', []),
+ 'num_pools': data.get('pgmap', {}).get('num_pools'),
+ 'num_pgs': data.get('pgmap', {}).get('num_pgs'),
+ 'bytes_used': data.get('pgmap', {}).get('bytes_used'),
+ 'bytes_total': data.get('pgmap', {}).get('bytes_total'),
+ }
+
+ if self._has_permissions(Permission.READ, Scope.MANAGER):
+ mgrmap = data.get('mgrmap', {})
+ available = mgrmap.get('available', False)
+ num_standbys = mgrmap.get('num_standbys')
+ num_active = 1 if available else 0
+ summary['mgrmap'] = {
+ 'num_active': num_active,
+ 'num_standbys': num_standbys,
+ }
+
+ if self._has_permissions(Permission.READ, Scope.CEPHFS):
+ fsmap = data.get('fsmap', {})
+ by_rank = fsmap.get('by_rank', [])
+
+ active_count = 0
+ standby_replay_count = 0
+
+ for mds in by_rank:
+ state = mds.get('status', '')
+ if state == 'up:standby-replay':
+ standby_replay_count += 1
+ elif state.startswith('up:'):
+ active_count += 1
+
+ summary['fsmap'] = {
+ 'num_active': active_count,
+ 'num_standbys': fsmap.get('up:standby', 0) + standby_replay_count,
+ }
+
+ if self._has_permissions(Permission.READ, Scope.RGW):
+ daemons = (
+ data.get('servicemap', {})
+ .get('services', {})
+ .get('rgw', {})
+ .get('daemons', {})
+ or {}
+ )
+ daemons.pop("summary", None)
+ summary['num_rgw_gateways'] = len(daemons)
+
+ if self._has_permissions(Permission.READ, Scope.ISCSI):
+ summary['num_iscsi_gateways'] = self.health_minimal.iscsi_daemons()
+
+ if self._has_permissions(Permission.READ, Scope.HOSTS):
+ summary['num_hosts'] = len(get_hosts())
+
+ return summary
"postcss-scss": "4.0.9",
"prettier": "2.1.2",
"pretty-quick": "3.0.2",
+ "purgecss": "7.0.2",
"start-server-and-test": "2.0.3",
"stylelint": "16.20.0",
"stylelint-config-standard": "38.0.0",
],
"license": "MIT"
},
+ "node_modules/purgecss": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/purgecss/-/purgecss-7.0.2.tgz",
+ "integrity": "sha512-4Ku8KoxNhOWi9X1XJ73XY5fv+I+hhTRedKpGs/2gaBKU8ijUiIKF/uyyIyh7Wo713bELSICF5/NswjcuOqYouQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^12.1.0",
+ "glob": "^11.0.0",
+ "postcss": "^8.4.47",
+ "postcss-selector-parser": "^6.1.2"
+ },
+ "bin": {
+ "purgecss": "bin/purgecss.js"
+ }
+ },
+ "node_modules/purgecss/node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/purgecss/node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/qs": {
"version": "6.10.4",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz",
"postcss-scss": "4.0.9",
"prettier": "2.1.2",
"pretty-quick": "3.0.2",
+ "purgecss": "7.0.2",
"start-server-and-test": "2.0.3",
"stylelint": "16.20.0",
- "stylelint-scss": "6.12.1",
"stylelint-config-standard": "38.0.0",
+ "stylelint-scss": "6.12.1",
"table": "6.8.0",
"ts-node": "10.9.2",
"typescript": "5.4.5",
"stepDefinitions": "cypress/e2e/common"
},
"optionalDependencies": {
- "@rollup/rollup-linux-arm64-gnu": "4.22.4"
+ "@rollup/rollup-linux-arm64-gnu": "4.22.4"
}
-}
\ No newline at end of file
+}
[dropdownData]="(isHardwareEnabled$ | async) && (hardwareSummary$ | async)">
</cd-card-row>
<!-- Monitors -->
- <cd-card-row [data]="monMap?.monmap.mons.length"
+ <cd-card-row [data]="monCount"
link="/monitor"
title="Monitor"
summaryType="simplified"></cd-card-row>
<!-- Managers -->
- <cd-card-row [data]="mgrMap | mgrSummary"
+ <cd-card-row [data]="mgrStatus"
title="Manager"></cd-card-row>
<!-- OSDs -->
- <cd-card-row [data]="osdMap | osdSummary"
+ <cd-card-row [data]="osdCount"
link="/osd"
title="OSD"
summaryType="osd"></cd-card-row>
<!-- Pools -->
- <cd-card-row [data]="poolStatus?.length"
+ <cd-card-row [data]="poolCount"
link="/pool"
title="Pool"
summaryType="simplified"></cd-card-row>
*ngIf="enabledFeature.rgw"></cd-card-row>
<!-- Metadata Servers -->
- <cd-card-row [data]="mdsMap | mdsSummary"
+ <cd-card-row [data]="mdsStatus"
title="Metadata Server"
id="mds-item"
*ngIf="enabledFeature.cephfs"></cd-card-row>
</div>
<div class="d-flex flex-column ms-4 me-4 mt-4 mb-4">
<div class="d-flex flex-row col-md-3 ms-4">
- <i *ngIf="healthData?.status else loadingTpl"
- [ngClass]="[healthData.status | healthIcon, icons.large2x]"
- [ngStyle]="healthData.status | healthColor"
- [title]="healthData.status">
+ <i *ngIf="healthCardData?.status else loadingTpl"
+ [ngClass]="[healthCardData.status | healthIcon, icons.large2x]"
+ [ngStyle]="healthCardData.status | healthColor"
+ [title]="healthCardData.status">
</i>
<span class="ms-2 mt-n1 lead"
- *ngIf="!healthData?.checks?.length"
+ *ngIf="!hasHealthChecks"
i18n>Cluster</span>
<cds-toggletip [dropShadow]="true"
[autoAlign]="true">
<div cdsToggletipButton>
<a class="ms-2 mt-n1 lead text-primary"
popoverClass="info-card-popover-cluster-status"
- *ngIf="healthData?.checks?.length"
+ *ngIf="hasHealthChecks"
i18n>Cluster
</a>
</div>
<div cdsToggletipContent
#healthCheck>
<div class="cds--popover-scroll-container">
- <cd-health-checks *ngIf="healthData?.checks"
- [healthData]="healthData.checks">
+ <cd-health-checks *ngIf="hasHealthChecks"
+ [healthData]="healthCardData.checks">
</cd-health-checks>
</div>
</div>
[fullHeight]="true"
aria-label="Capacity card">
<ng-container class="ms-4 me-4"
- *ngIf="capacity">
- <cd-dashboard-pie [data]="{max: capacity.total_bytes, current: capacity.total_used_raw_bytes}"
+ *ngIf="totalCapacity && usedCapacity">
+ <cd-dashboard-pie [data]="{max: totalCapacity, current: usedCapacity}"
[lowThreshold]="capacityCardData.osdNearfull"
[highThreshold]="capacityCardData.osdFull">
</cd-dashboard-pie>
<div class="ms-4 me-4 mt-0">
<cd-dashboard-time-selector (selectedTime)="getPrometheusData($event)">
</cd-dashboard-time-selector>
- <ng-container *ngIf="capacity">
+ <ng-container *ngIf="usedCapacity">
<cd-dashboard-area-chart chartTitle="Used Capacity (RAW)"
- [maxValue]="capacity.total_bytes"
+ [maxValue]="usedCapacity"
dataUnits="B"
[labelsArray]="['Used Capacity']"
[dataArray]="[queriesResults.USEDCAPACITY]">
</ng-container>
</ng-template>
-<ng-template #logsLink>
- <ng-container *ngIf="permissions.log.read">
- <p class="logs-link"
- i18n><i [ngClass]="[icons.infoCircle]"></i> See <a routerLink="/logs">Logs</a> for more details.</p>
- </ng-container>
-</ng-template>
-
<ng-template #loadingTpl>
<cds-inline-loading></cds-inline-loading>
</ng-template>
import { DashboardV3Component } from './dashboard-v3.component';
import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
import { AlertClass } from '~/app/shared/enum/health-icon.enum';
+import { HealthSnapshotMap } from '~/app/shared/models/health.interface';
export class SummaryServiceMock {
summaryDataSource = new BehaviorSubject({
describe('Dashbord Component', () => {
let component: DashboardV3Component;
let fixture: ComponentFixture<DashboardV3Component>;
- let healthService: HealthService;
let orchestratorService: OrchestratorService;
- let getHealthSpy: jasmine.Spy;
+ let getHealthStatusSpy: jasmine.Spy;
let getAlertsSpy: jasmine.Spy;
let fakeFeatureTogglesService: jasmine.Spy;
- const healthPayload: Record<string, any> = {
- health: { status: 'HEALTH_OK' },
- mon_status: { monmap: { mons: [] }, quorum: [] },
- osd_map: { osds: [] },
- mgr_map: { standbys: [] },
- hosts: 0,
- rgw: 0,
- fs_map: { filesystems: [], standbys: [] },
- iscsi_daemons: 1,
- client_perf: {},
- scrub_status: 'Inactive',
- pools: [],
- df: { stats: {} },
- pg_info: { object_stats: { num_objects: 1 } }
+ const healthStatusPayload: HealthSnapshotMap = {
+ fsid: '7d0cc9da-ca8d-4539-a953-ab062139c26a',
+ health: {
+ status: 'HEALTH_WARN',
+ checks: {
+ DASHBOARD_DEBUG: {
+ severity: 'HEALTH_WARN',
+ summary: {
+ message: 'Dashboard debug mode is enabled',
+ count: 0
+ },
+ muted: false
+ }
+ },
+ mutes: []
+ },
+ monmap: {
+ num_mons: 3
+ },
+ osdmap: {
+ in: 3,
+ up: 3,
+ num_osds: 3
+ },
+ pgmap: {
+ pgs_by_state: [
+ {
+ state_name: 'active+clean',
+ count: 497
+ }
+ ],
+ num_pools: 14,
+ bytes_used: 3236978688,
+ bytes_total: 325343772672,
+ num_pgs: 497
+ },
+ mgrmap: {
+ num_active: 1,
+ num_standbys: 0
+ },
+ fsmap: {
+ num_standbys: 2,
+ num_active: 1
+ },
+ num_rgw_gateways: 3,
+ num_iscsi_gateways: {
+ up: 0,
+ down: 0
+ },
+ num_hosts: 1
};
const alertsPayload: AlertmanagerAlert[] = [
}
];
- const configValueData: any = 'e90a0d58-658e-4148-8f61-e896c86f0696';
-
const orchName: any = 'Cephadm';
configureTestBed({
);
fixture = TestBed.createComponent(DashboardV3Component);
component = fixture.componentInstance;
- healthService = TestBed.inject(HealthService);
orchestratorService = TestBed.inject(OrchestratorService);
- getHealthSpy = spyOn(TestBed.inject(HealthService), 'getMinimalHealth');
- getHealthSpy.and.returnValue(of(healthPayload));
+ getHealthStatusSpy = spyOn(TestBed.inject(HealthService), 'getHealthSnapshot');
+ getHealthStatusSpy.and.returnValue(of(healthStatusPayload));
getAlertsSpy = spyOn(TestBed.inject(PrometheusService), 'getAlerts');
getAlertsSpy.and.returnValue(of(alertsPayload));
component.prometheusAlertService.alerts = alertsPayload;
});
it('should get corresponding data into detailsCardData', () => {
- spyOn(healthService, 'getClusterFsid').and.returnValue(of(configValueData));
spyOn(orchestratorService, 'getName').and.returnValue(of(orchName));
component.ngOnInit();
- expect(component.detailsCardData.fsid).toBe('e90a0d58-658e-4148-8f61-e896c86f0696');
+ expect(component.detailsCardData.fsid).toBe(healthStatusPayload['fsid']);
expect(component.detailsCardData.orchestrator).toBe('Cephadm');
expect(component.detailsCardData.cephVersion).toBe('17.0.0-12222-gcd0cd7cb quincy (dev)');
});
it('should check if the respective icon is shown for each status', () => {
- const payload = _.cloneDeep(healthPayload);
+ const payload = _.cloneDeep(healthStatusPayload);
// HEALTH_WARN
payload.health['status'] = 'HEALTH_WARN';
- payload.health['checks'] = [
- { severity: 'HEALTH_WARN', type: 'WRN', summary: { message: 'fake warning' } }
- ];
-
- getHealthSpy.and.returnValue(of(payload));
+ payload.health['checks'] = {
+ FAKE_CHECK: {
+ severity: 'HEALTH_WARN',
+ summary: { message: 'fake warning', count: 1 },
+ muted: false
+ }
+ };
+
+ getHealthStatusSpy.and.returnValue(of(payload));
fixture.detectChanges();
const clusterStatusCard = fixture.debugElement.query(By.css('cd-card[cardTitle="Status"] i'));
expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`);
// HEALTH_ERR
payload.health['status'] = 'HEALTH_ERR';
- payload.health['checks'] = [
- { severity: 'HEALTH_ERR', type: 'ERR', summary: { message: 'fake error' } }
- ];
-
- getHealthSpy.and.returnValue(of(payload));
+ payload.health['checks'] = {
+ FAKE_CHECK: {
+ severity: 'HEALTH_ERR',
+ summary: { message: 'fake error', count: 1 },
+ muted: false
+ }
+ };
+
+ getHealthStatusSpy.and.returnValue(of(payload));
fixture.detectChanges();
expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`);
// HEALTH_OK
payload.health['status'] = 'HEALTH_OK';
- payload.health['checks'] = [
- { severity: 'HEALTH_OK', type: 'OK', summary: { message: 'fake success' } }
- ];
+ payload.health['checks'] = {};
- getHealthSpy.and.returnValue(of(payload));
+ getHealthStatusSpy.and.returnValue(of(payload));
fixture.detectChanges();
expect(clusterStatusCard.nativeElement.title).toEqual(`${payload.health.status}`);
});
});
it('should render "Status" card text that is not clickable', () => {
- fixture.detectChanges();
+ const payload = _.cloneDeep(healthStatusPayload);
+ payload.health['status'] = 'HEALTH_OK';
+ payload.health['checks'] = null;
+ getHealthStatusSpy.and.returnValue(of(payload));
+ fixture.detectChanges();
const clusterStatusCard = fixture.debugElement.query(By.css('cd-card[cardTitle="Status"]'));
const clickableContent = clusterStatusCard.query(By.css('.lead.text-primary'));
expect(clickableContent).toBeNull();
});
it('should render "Status" card text that is clickable (popover)', () => {
- const payload = _.cloneDeep(healthPayload);
+ const payload = _.cloneDeep(healthStatusPayload);
payload.health['status'] = 'HEALTH_WARN';
- payload.health['checks'] = [
- { severity: 'HEALTH_WARN', type: 'WRN', summary: { message: 'fake warning' } }
- ];
-
- getHealthSpy.and.returnValue(of(payload));
+ payload.health['checks'] = {
+ FAKE_CHECK: {
+ severity: 'HEALTH_WARN',
+ summary: { message: 'fake warning', count: 1 },
+ muted: false
+ }
+ };
+
+ getHealthStatusSpy.and.returnValue(of(payload));
fixture.detectChanges();
const clusterStatusCard = fixture.debugElement.query(By.css('cd-card[cardTitle="Status"]'));
UtilizationCardQueries
} from '~/app/shared/enum/dashboard-promqls.enum';
import { Icons } from '~/app/shared/enum/icons.enum';
-import { DashboardDetails } from '~/app/shared/models/cd-details';
+import {
+ CapacityCardDetails,
+ DashboardDetails,
+ InventoryCommonDetail,
+ InventoryDetails
+} from '~/app/shared/models/cd-details';
import { Permissions } from '~/app/shared/models/permissions';
import { AlertmanagerAlert } from '~/app/shared/models/prometheus-alerts';
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
import { HardwareService } from '~/app/shared/api/hardware.service';
import { SettingsService } from '~/app/shared/api/settings.service';
import {
+ Health,
+ HealthSnapshotMap,
IscsiMap,
- MdsMap,
- MgrMap,
- MonMap,
- OsdMap,
- PgStatus
+ PgStateCount
} from '~/app/shared/models/health.interface';
-type CapacityCardData = {
- osdNearfull: number;
- osdFull: number;
-};
-
@Component({
selector: 'cd-dashboard-v3',
templateUrl: './dashboard-v3.component.html',
styleUrls: ['./dashboard-v3.component.scss']
})
export class DashboardV3Component extends PrometheusListHelper implements OnInit, OnDestroy {
- detailsCardData: DashboardDetails = {};
- capacityCardData: CapacityCardData = {
- osdNearfull: null,
- osdFull: null
- };
- interval = new Subscription();
+ telemetryURL = 'https://telemetry-public.ceph.com/';
+ origin = window.location.origin;
+ icons = Icons;
+
permissions: Permissions;
+
+ hardwareSubject = new BehaviorSubject<any>([]);
+ private subs = new Subscription();
+ private destroy$ = new Subject<void>();
+
enabledFeature$: FeatureTogglesMap$;
- color: string;
- capacityService: any;
- capacity: any;
- healthData$: Observable<Object>;
prometheusAlerts$: Observable<AlertmanagerAlert[]>;
+ isHardwareEnabled$: Observable<boolean>;
+ hardwareSummary$: Observable<any>;
+ managedByConfig$: Observable<any>;
- icons = Icons;
+ color: string;
flexHeight = true;
simplebar = {
autoHide: true
borderClass: string;
alertType: string;
alertClass = AlertClass;
- healthData: any;
- categoryPgAmount: Record<string, number> = {};
- totalPgs = 0;
+
queriesResults: { [key: string]: [] } = {
USEDCAPACITY: [],
IPS: [],
READIOPS: [],
WRITEIOPS: []
};
+
telemetryEnabled: boolean;
- telemetryURL = 'https://telemetry-public.ceph.com/';
- origin = window.location.origin;
+ detailsCardData: DashboardDetails = {};
+ capacityCardData: CapacityCardDetails = {
+ osdNearfull: null,
+ osdFull: null
+ };
+ healthCardData: Health;
+ hasHealthChecks: boolean;
hardwareHealth: any;
hardwareEnabled: boolean = false;
hasHardwareError: boolean = false;
- isHardwareEnabled$: Observable<boolean>;
- hardwareSummary$: Observable<any>;
- hardwareSubject = new BehaviorSubject<any>([]);
- managedByConfig$: Observable<any>;
- private subs = new Subscription();
- private destroy$ = new Subject<void>();
-
+ totalCapacity: number = null;
+ usedCapacity: number = null;
hostsCount: number = null;
- monMap: MonMap = null;
- mgrMap: MgrMap = null;
- osdMap: OsdMap = null;
- poolStatus: Record<string, any>[] = null;
- pgStatus: PgStatus = null;
+ monCount: number = null;
+ poolCount: number = null;
rgwCount: number = null;
- mdsMap: MdsMap = null;
+ osdCount: { in: number; out: number; up: number; down: number } & InventoryCommonDetail = null;
+ pgStatus: { statuses: PgStateCount[] } & InventoryCommonDetail = null;
+ mgrStatus: InventoryDetails = null;
+ mdsStatus: InventoryDetails = null;
iscsiMap: IscsiMap = null;
constructor(
}
this.loadInventories();
-
- // fetch capacity to load the capacity chart
- this.refreshIntervalObs(() => this.healthService.getClusterCapacity()).subscribe({
- next: (capacity: any) => {
- this.capacity = capacity;
- }
- });
-
this.getPrometheusData(this.prometheusService.lastHourDateObject);
this.getDetailsCardData();
this.getTelemetryReport();
getTelemetryText(): string {
return this.telemetryEnabled
- ? 'Cluster telemetry is active'
- : 'Cluster telemetry is inactive. To Activate the Telemetry, \
+ ? $localize`Cluster telemetry is active`
+ : $localize`Cluster telemetry is inactive. To Activate the Telemetry, \
click settings icon on top navigation bar and select \
- Telemetry configration.';
+ Telemetry configration.`;
}
+
ngOnDestroy() {
this.prometheusService.unsubscribe();
this.subs?.unsubscribe();
}
getDetailsCardData() {
- this.healthService.getClusterFsid().subscribe((data: string) => {
- this.detailsCardData.fsid = data;
- });
this.orchestratorService.getName().subscribe((data: string) => {
this.detailsCardData.orchestrator = data;
});
);
}
+ private safeSum(a: number, b: number): number | null {
+ return a != null && b != null ? a + b : null;
+ }
+
+ private safeDifference(a: number, b: number): number | null {
+ return a != null && b != null ? a - b : null;
+ }
+
loadInventories() {
- this.refreshIntervalObs(() => this.healthService.getMinimalHealth()).subscribe({
- next: (result: any) => {
- this.hostsCount = result.hosts;
- this.monMap = result.mon_status;
- this.mgrMap = result.mgr_map;
- this.osdMap = result.osd_map;
- this.poolStatus = result.pools;
- this.pgStatus = result.pg_info;
- this.rgwCount = result.rgw;
- this.mdsMap = result.fs_map;
- this.iscsiMap = result.iscsi_daemons;
- this.healthData = result.health;
+ this.refreshIntervalObs(() => this.healthService.getHealthSnapshot()).subscribe({
+ next: (data: HealthSnapshotMap) => {
+ this.detailsCardData.fsid = data?.fsid;
+ this.healthCardData = data?.health;
+ this.hasHealthChecks = !!Object.keys(this.healthCardData?.checks ?? {})?.length;
+ this.monCount = data?.monmap?.num_mons;
+
+ const osdMap = data?.osdmap;
+ const osdIn = osdMap?.in;
+ const osdUp = osdMap?.up;
+ const osdTotal = osdMap?.num_osds;
+
+ this.osdCount = {
+ in: osdIn,
+ up: osdUp,
+ total: osdTotal,
+ down: this.safeDifference(osdTotal, osdUp),
+ out: this.safeDifference(osdTotal, osdIn)
+ };
+
+ const pgmap = data?.pgmap;
+ this.poolCount = pgmap?.num_pools;
+ this.usedCapacity = pgmap?.bytes_used;
+ this.totalCapacity = pgmap?.bytes_total;
+ this.pgStatus = {
+ statuses: pgmap?.pgs_by_state,
+ total: pgmap?.num_pgs
+ };
+
+ const mgrmap = data?.mgrmap;
+ const mgrInfo = mgrmap?.num_standbys;
+ const mgrSuccess = mgrmap?.num_active;
+
+ this.mgrStatus = {
+ info: mgrInfo,
+ success: mgrSuccess,
+ total: this.safeSum(mgrInfo, mgrSuccess)
+ };
+
+ const mdsInfo = data?.fsmap?.num_standbys;
+ const mdsSuccess = data?.fsmap?.num_active;
+
+ this.mdsStatus = {
+ info: mdsInfo,
+ success: mdsSuccess,
+ total: this.safeSum(mdsInfo, mdsSuccess)
+ };
+
+ this.rgwCount = data?.num_rgw_gateways;
+ this.iscsiMap = data?.num_iscsi_gateways;
+ this.hostsCount = data?.num_hosts;
this.enabledFeature$ = this.featureToggles.get();
}
});
import { PgCategoryService } from '../shared/pg-category.service';
import { PgSummaryPipe } from './pg-summary.pipe';
-describe('OsdSummaryPipe', () => {
+describe('PgSummaryPipe', () => {
let pipe: PgSummaryPipe;
configureTestBed({
it('tranforms value', () => {
const value = {
- statuses: {
- 'active+clean': 241
- },
- pgs_per_osd: 241
+ statuses: [
+ {
+ state_name: 'active+clean',
+ count: 497
+ }
+ ],
+ total: 497
};
expect(pipe.transform(value)).toEqual({
categoryPgAmount: {
- clean: 241
+ clean: 497
},
- total: 241
+ total: 497
});
});
});
import { Pipe, PipeTransform } from '@angular/core';
-import _ from 'lodash';
import { PgCategoryService } from '~/app/ceph/shared/pg-category.service';
+import { PgStateCount } from '~/app/shared/models/health.interface';
@Pipe({
name: 'pgSummary'
transform(value: any): any {
if (!value) return null;
const categoryPgAmount: Record<string, number> = {};
- let total = 0;
- _.forEach(value.statuses, (pgAmount, pgStatesText) => {
- const categoryType = this.pgCategoryService.getTypeByStates(pgStatesText);
- if (_.isUndefined(categoryPgAmount[categoryType])) {
+ value.statuses.forEach((status: PgStateCount) => {
+ const categoryType = this.pgCategoryService.getTypeByStates(status?.state_name);
+ if (!categoryPgAmount?.[categoryType]) {
categoryPgAmount[categoryType] = 0;
}
- categoryPgAmount[categoryType] += pgAmount;
- total += pgAmount;
+ categoryPgAmount[categoryType] += status?.count;
});
return {
categoryPgAmount,
- total
+ total: value.total
};
}
}
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
import { PipesModule } from '~/app/shared/pipes/pipes.module';
import { HealthChecksComponent } from './health-checks/health-checks.component';
@NgModule({
- imports: [CommonModule, DataTableModule, SharedModule, NgbNavModule, PipesModule],
+ imports: [CommonModule, DataTableModule, SharedModule, NgbNavModule, PipesModule, RouterModule],
exports: [DeviceListComponent, SmartListComponent, HealthChecksComponent],
declarations: [DeviceListComponent, SmartListComponent, HealthChecksComponent]
})
-<ng-container *ngTemplateOutlet="logsLink"></ng-container>
+<p i18n
+ *ngIf="permissions.log.read">
+ <cd-icon type="infoCircle"></cd-icon >
+ See <a routerLink="/logs">Logs</a> for more details.
+</p>
<ul>
- <li *ngFor="let check of healthData">
- <span [ngStyle]="check.severity | healthColor"
- [class.health-warn-description]="check.severity === 'HEALTH_WARN'">
- {{ check.type }}</span>: {{ check.summary.message }} <br>
- <div *ngIf="check.type === 'CEPHADM_FAILED_DAEMON'"
- class="failed-daemons">
- <cd-help-text>
- <b>Failed Daemons:</b>
- <div *ngFor="let failedDaemons of getFailedDaemons(check.detail); let last = last">
- {{ failedDaemons }}
- {{ !last ? ', ' : '' }}
- </div>
- </cd-help-text>
- </div>
- <div *ngFor="let details of check?.detail">
- <cd-help-text>{{ details?.message }}</cd-help-text>
- </div>
+ <li *ngFor="let check of healthData | keyvalue">
+ <span [ngStyle]="check.value.severity | healthColor"
+ [class.health-warn-description]="check.value.severity === 'HEALTH_WARN'">
+ {{ check.key }}</span>: {{ check.value.summary.message }} <br>
</li>
</ul>
-
-<ng-template #logsLink>
- <ng-container *ngIf="permissions.log.read">
- <p class="logs-link"
- i18n><i [ngClass]="[icons.infoCircle]"></i> See <a routerLink="/logs">Logs</a> for more details.</p>
- </ng-container>
-</ng-template>
import { HealthChecksComponent } from './health-checks.component';
import { HealthColorPipe } from '~/app/shared/pipes/health-color.pipe';
-import { By } from '@angular/platform-browser';
import { CssHelper } from '~/app/shared/classes/css-helper';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
it('should create', () => {
expect(component).toBeTruthy();
});
-
- it('should show the correct health warning for failed daemons', () => {
- component.healthData = [
- {
- severity: 'HEALTH_WARN',
- summary: {
- message: '1 failed cephadm daemon(s)',
- count: 1
- },
- detail: [
- {
- message: 'daemon ceph-exporter.ceph-node-00 on ceph-node-00 is in error state'
- }
- ],
- muted: false,
- type: 'CEPHADM_FAILED_DAEMON'
- }
- ];
- fixture.detectChanges();
- const failedDaemons = fixture.debugElement.query(By.css('.failed-daemons'));
- expect(failedDaemons.nativeElement.textContent).toContain(
- 'Failed Daemons: ceph-exporter.ceph-node-00 '
- );
- });
});
const req = httpTesting.expectOne('api/health/minimal');
expect(req.request.method).toBe('GET');
});
+
+ it('should call getHealthSnapshot', () => {
+ service.getHealthSnapshot().subscribe();
+ const req = httpTesting.expectOne('api/health/snapshot');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call getClusterFsid', () => {
+ service.getClusterFsid().subscribe();
+ const req = httpTesting.expectOne('api/health/get_cluster_fsid');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call getOrchestratorName', () => {
+ service.getOrchestratorName().subscribe();
+ const req = httpTesting.expectOne('api/health/get_orchestrator_name');
+ expect(req.request.method).toBe('GET');
+ });
+
+ it('should call getTelemetryStatus', () => {
+ service.getTelemetryStatus().subscribe();
+ const req = httpTesting.expectOne('api/health/get_telemetry_status');
+ expect(req.request.method).toBe('GET');
+ });
});
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs';
+import { HealthSnapshotMap } from '../models/health.interface';
+
+const BASE_URL = 'api/health';
@Injectable({
providedIn: 'root'
constructor(private http: HttpClient) {}
getFullHealth() {
- return this.http.get('api/health/full');
+ return this.http.get(`${BASE_URL}/full`);
}
getMinimalHealth() {
- return this.http.get('api/health/minimal');
+ return this.http.get(`${BASE_URL}/minimal`);
}
- getClusterCapacity() {
- return this.http.get('api/health/get_cluster_capacity');
+ getHealthSnapshot(): Observable<HealthSnapshotMap> {
+ return this.http.get<HealthSnapshotMap>(`${BASE_URL}/snapshot`);
}
getClusterFsid() {
- return this.http.get('api/health/get_cluster_fsid');
+ return this.http.get(`${BASE_URL}/get_cluster_fsid`);
}
getOrchestratorName() {
- return this.http.get('api/health/get_orchestrator_name');
+ return this.http.get(`${BASE_URL}/get_orchestrator_name`);
}
getTelemetryStatus() {
- return this.http.get('api/health/get_telemetry_status');
+ return this.http.get(`${BASE_URL}/get_telemetry_status`);
}
}
orchestrator?: string;
cephVersion?: string;
}
+
+export interface CapacityCardDetails {
+ osdNearfull: number;
+ osdFull: number;
+}
+
+export interface InventoryCommonDetail {
+ total: number;
+}
+
+export interface InventoryDetails extends InventoryCommonDetail {
+ info: number;
+ success: number;
+}
-export interface MonMap {
- monmap: {
- mons: Record<string, any>[];
- };
- quorum: number[];
-}
-
-export interface MgrMap {
- active_name: string;
- standbys: string[];
+export interface IscsiMap {
+ up: number;
+ down: number;
}
-export interface OsdMap {
- osds: Osd[];
+export interface HealthCheck {
+ severity: string;
+ summary: {
+ message: string;
+ count: number;
+ };
+ muted: boolean;
}
-export interface PgStatus {
- object_stats: ObjectStats;
- statuses: Status;
- pgs_per_osd: number;
+export interface Health {
+ status: string;
+ checks: Record<string, HealthCheck>;
+ mutes: string[];
}
-export interface MdsMap {
- filesystems: any[];
- standbys: any[];
+export interface MonMap {
+ num_mons: number;
}
-export interface IscsiMap {
+export interface OsdMap {
+ in: number;
up: number;
- down: number;
+ num_osds: number;
}
-interface ObjectStats {
- num_objects: number;
- num_object_copies: number;
- num_objects_degraded: number;
- num_objects_misplaced: number;
- num_objects_unfound: number;
+export interface PgStateCount {
+ state_name: string;
+ count: number;
+}
+export interface PgMap {
+ pgs_by_state: PgStateCount[];
+ num_pools: number;
+ bytes_used: number;
+ bytes_total: number;
+ num_pgs: number;
}
-interface Status {
- 'active+clean': number;
+export interface HealthMapCommon {
+ num_standbys: number;
+ num_active: number;
}
-interface Osd {
- in: number;
- up: number;
- state: string[];
+export interface HealthSnapshotMap {
+ fsid: string;
+ health: Health;
+ monmap: MonMap;
+ osdmap: OsdMap;
+ pgmap: PgMap;
+ mgrmap: HealthMapCommon;
+ fsmap: HealthMapCommon;
+ num_rgw_gateways: number;
+ num_iscsi_gateways: { up: number; down: number };
+ num_hosts: number;
}
+++ /dev/null
-import { TestBed } from '@angular/core/testing';
-
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { MdsSummaryPipe } from './mds-summary.pipe';
-
-describe('MdsSummaryPipe', () => {
- let pipe: MdsSummaryPipe;
-
- configureTestBed({
- providers: [MdsSummaryPipe]
- });
-
- beforeEach(() => {
- pipe = TestBed.inject(MdsSummaryPipe);
- });
-
- it('create an instance', () => {
- expect(pipe).toBeTruthy();
- });
-
- it('transforms with 0 active and 2 standy', () => {
- const payload = {
- standbys: [{ name: 'a' }],
- filesystems: [{ mdsmap: { info: [{ state: 'up:standby-replay' }] } }]
- };
-
- expect(pipe.transform(payload)).toEqual({
- success: 0,
- info: 2,
- total: 2
- });
- });
-
- it('transforms with 1 active and 1 standy', () => {
- const payload = {
- standbys: [{ name: 'b' }],
- filesystems: [{ mdsmap: { info: [{ state: 'up:active', name: 'a' }] } }]
- };
- expect(pipe.transform(payload)).toEqual({
- success: 1,
- info: 1,
- total: 2
- });
- });
-
- it('transforms with 0 filesystems', () => {
- const payload: Record<string, any> = {
- standbys: [0],
- filesystems: []
- };
-
- expect(pipe.transform(payload)).toEqual({
- success: 0,
- info: 0,
- total: 0
- });
- });
-
- it('transforms without filesystem', () => {
- const payload = { standbys: [{ name: 'a' }] };
-
- expect(pipe.transform(payload)).toEqual({
- success: 0,
- info: 1,
- total: 1
- });
- });
-
- it('transforms without value', () => {
- expect(pipe.transform(undefined)).toEqual(null);
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-
-import _ from 'lodash';
-
-@Pipe({
- name: 'mdsSummary'
-})
-export class MdsSummaryPipe implements PipeTransform {
- transform(value: any): any {
- if (!value) {
- return null;
- }
-
- let activeCount = 0;
- let standbyCount = 0;
- let standbys = 0;
- let active = 0;
- let standbyReplay = 0;
- _.each(value.standbys, () => {
- standbys += 1;
- });
-
- if (value.standbys && !value.filesystems) {
- standbyCount = standbys;
- activeCount = 0;
- } else if (value.filesystems.length === 0) {
- activeCount = 0;
- } else {
- _.each(value.filesystems, (fs) => {
- _.each(fs.mdsmap.info, (mds) => {
- if (mds.state === 'up:standby-replay') {
- standbyReplay += 1;
- } else {
- active += 1;
- }
- });
- });
-
- activeCount = active;
- standbyCount = standbys + standbyReplay;
- }
- const totalCount = activeCount + standbyCount;
- const mdsSummary = {
- success: activeCount,
- info: standbyCount,
- total: totalCount
- };
-
- return mdsSummary;
- }
-}
+++ /dev/null
-import { TestBed } from '@angular/core/testing';
-
-import { configureTestBed } from '~/testing/unit-test-helper';
-import { OsdSummaryPipe } from './osd-summary.pipe';
-
-describe('OsdSummaryPipe', () => {
- let pipe: OsdSummaryPipe;
-
- configureTestBed({
- providers: [OsdSummaryPipe]
- });
-
- beforeEach(() => {
- pipe = TestBed.inject(OsdSummaryPipe);
- });
-
- it('create an instance', () => {
- expect(pipe).toBeTruthy();
- });
-
- it('transforms without value', () => {
- expect(pipe.transform(undefined)).toBe(null);
- });
-
- it('transforms having 3 osd with 3 up, 3 in, 0 down, 0 out', () => {
- const value = {
- osds: [
- { up: 1, in: 1, state: ['up', 'exists'] },
- { up: 1, in: 1, state: ['up', 'exists'] },
- { up: 1, in: 1, state: ['up', 'exists'] }
- ]
- };
- expect(pipe.transform(value)).toEqual({
- total: 3,
- down: 0,
- out: 0,
- up: 3,
- in: 3,
- nearfull: 0,
- full: 0
- });
- });
-});
+++ /dev/null
-import { Pipe, PipeTransform } from '@angular/core';
-
-import _ from 'lodash';
-
-@Pipe({
- name: 'osdSummary'
-})
-export class OsdSummaryPipe implements PipeTransform {
- transform(value: any): any {
- if (!value) {
- return null;
- }
-
- let inCount = 0;
- let upCount = 0;
- let nearFullCount = 0;
- let fullCount = 0;
- _.each(value.osds, (osd) => {
- if (osd.in) {
- inCount++;
- }
- if (osd.up) {
- upCount++;
- }
- if (osd.state.includes('nearfull')) {
- nearFullCount++;
- }
- if (osd.state.includes('full')) {
- fullCount++;
- }
- });
-
- const downCount = value.osds.length - upCount;
- const outCount = value.osds.length - inCount;
- const osdSummary = {
- total: value.osds.length,
- down: downCount,
- out: outCount,
- up: upCount,
- in: inCount,
- nearfull: nearFullCount,
- full: fullCount
- };
- return osdSummary;
- }
-}
import { JoinPipe } from './join.pipe';
import { LogPriorityPipe } from './log-priority.pipe';
import { MapPipe } from './map.pipe';
-import { MdsSummaryPipe } from './mds-summary.pipe';
import { MgrSummaryPipe } from './mgr-summary.pipe';
import { MillisecondsPipe } from './milliseconds.pipe';
import { NotAvailablePipe } from './not-available.pipe';
import { OrdinalPipe } from './ordinal.pipe';
-import { OsdSummaryPipe } from './osd-summary.pipe';
import { RbdConfigurationSourcePipe } from './rbd-configuration-source.pipe';
import { RelativeDatePipe } from './relative-date.pipe';
import { RoundPipe } from './round.pipe';
SearchHighlightPipe,
HealthIconPipe,
MgrSummaryPipe,
- MdsSummaryPipe,
- OsdSummaryPipe,
OctalToHumanReadablePipe,
PathPipe,
PluralizePipe,
SearchHighlightPipe,
HealthIconPipe,
MgrSummaryPipe,
- MdsSummaryPipe,
- OsdSummaryPipe,
OctalToHumanReadablePipe,
PathPipe,
PluralizePipe,
SanitizeHtmlPipe,
HealthIconPipe,
MgrSummaryPipe,
- MdsSummaryPipe,
- OsdSummaryPipe,
OctalToHumanReadablePipe,
MbpersecondPipe,
DimlessBinaryPerMinutePipe
trace.
security:
- jwt: []
+ summary: Get Cluster's detailed health report
tags:
- Health
/api/health/get_cluster_capacity:
trace.
security:
- jwt: []
- summary: Get Cluster's minimal health report
+ summary: Get Cluster's health report with lesser details
+ tags:
+ - Health
+ /api/health/snapshot:
+ get:
+ parameters: []
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ fsid:
+ description: Cluster filesystem ID
+ type: string
+ fsmap:
+ description: Filesystem map details
+ properties:
+ num_active:
+ description: Number of active mds
+ type: integer
+ num_standbys:
+ description: Standby MDS count
+ type: integer
+ required:
+ - num_active
+ - num_standbys
+ type: object
+ health:
+ description: Cluster health overview
+ properties:
+ checks:
+ description: Health checks keyed by name
+ properties:
+ <check_name>:
+ description: Individual health check object
+ properties:
+ muted:
+ description: Whether the check is muted
+ type: boolean
+ severity:
+ description: Health severity level
+ type: string
+ summary:
+ description: Summary details
+ properties:
+ count:
+ description: Occurrence count
+ type: integer
+ message:
+ description: Human-readable summary
+ type: string
+ required:
+ - message
+ - count
+ type: object
+ required:
+ - severity
+ - summary
+ - muted
+ type: object
+ required:
+ - <check_name>
+ type: object
+ mutes:
+ description: List of muted check names
+ items:
+ type: string
+ type: array
+ status:
+ description: Overall health status
+ type: string
+ required:
+ - status
+ - checks
+ - mutes
+ type: object
+ mgrmap:
+ description: Manager map details
+ properties:
+ num_active:
+ description: Number of active managers
+ type: integer
+ num_standbys:
+ description: Standby manager count
+ type: integer
+ required:
+ - num_active
+ - num_standbys
+ type: object
+ monmap:
+ description: Monitor map details
+ properties:
+ num_mons:
+ description: Number of monitors
+ type: integer
+ required:
+ - num_mons
+ type: object
+ num_iscsi_gateways:
+ description: Iscsi gateways status
+ properties:
+ down:
+ description: Count of iSCSI gateways not running
+ type: integer
+ up:
+ description: Count of iSCSI gateways running
+ type: integer
+ required:
+ - up
+ - down
+ type: object
+ num_rgw_gateways:
+ description: Count of RGW gateway daemons running
+ type: integer
+ osdmap:
+ description: OSD map details
+ properties:
+ in:
+ description: Number of OSDs in
+ type: integer
+ num_osds:
+ description: Total OSD count
+ type: integer
+ up:
+ description: Number of OSDs up
+ type: integer
+ required:
+ - in
+ - up
+ - num_osds
+ type: object
+ pgmap:
+ description: Placement group map details
+ properties:
+ bytes_total:
+ description: Total capacity in bytes
+ type: integer
+ bytes_used:
+ description: Used capacity in bytes
+ type: integer
+ num_pgs:
+ description: Total PG count
+ type: integer
+ num_pools:
+ description: Number of pools
+ type: integer
+ pgs_by_state:
+ description: List of PG counts by state
+ items:
+ properties:
+ count:
+ description: Count of PGs in this state
+ type: integer
+ state_name:
+ description: Placement group state
+ type: string
+ required:
+ - state_name
+ - count
+ type: object
+ type: array
+ required:
+ - pgs_by_state
+ - num_pools
+ - num_pgs
+ - bytes_used
+ - bytes_total
+ type: object
+ required:
+ - fsid
+ - health
+ - monmap
+ - osdmap
+ - pgmap
+ - mgrmap
+ - fsmap
+ - num_rgw_gateways
+ - num_iscsi_gateways
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Get a quick overview of cluster health at a moment, analogous to the
+ ceph status command in CLI.
tags:
- Health
/api/host: