]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Add /health/snapshot api 65019/head
authorAfreen Misbah <afreen@ibm.com>
Wed, 13 Aug 2025 06:49:02 +0000 (12:19 +0530)
committerAfreen Misbah <afreen@ibm.com>
Mon, 25 Aug 2025 10:59:23 +0000 (16:29 +0530)
Fixes https://tracker.ceph.com/issues/72609

- The current minimal API relies on fetching data from osdmap and pgmap.
- These commands produce large, detailed payloads that become a performance bottleneck and impact scalability, especially in large clusters.
- To address this, we propose switching to the ceph snapshot API using ceph status command, which retrieves essential information directly from the cluster map.
- ceph status is significantly more lightweight compared to osdmap/pgmap, reducing payload sizes and processing overhead.
- This change ensures faster response times, improves system efficiency in large deployments, and minimizes unnecessary data transfer.
- update tests

Signed-off-by: Afreen Misbah <afreen@ibm.com>
21 files changed:
src/pybind/mgr/dashboard/controllers/health.py
src/pybind/mgr/dashboard/frontend/package-lock.json
src/pybind/mgr/dashboard/frontend/package.json
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/dashboard/dashboard-v3.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/pg-summary.pipe.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard-v3/pg-summary.pipe.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/ceph-shared.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/health-checks/health-checks.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/health-checks/health-checks.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/health.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/health.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/cd-details.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/health.interface.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.spec.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.ts [deleted file]
src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/pipes.module.ts
src/pybind/mgr/dashboard/openapi.yaml

index de45bebbb465ba12a929d1379900c3c5ade480dd..1457ab494c19c53f8b31c8209abcbcaa5c38596e 100644 (file)
@@ -110,6 +110,55 @@ HEALTH_MINIMAL_SCHEMA = ({
     '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):
     """
@@ -281,15 +330,28 @@ 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()
@@ -305,3 +367,87 @@ class Health(BaseController):
     @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
index 74d99510fe6f8169a2ba65f01fe65b63e61e6b36..be631f2f3c33c02dc4e3404e7696c5f8cc950b2a 100644 (file)
         "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",
index 0838725e86d054b0405e6a1caab10c7463c91317..b158a580c3ab2d87facd11c6d83a07c365546307 100644 (file)
     "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
+}
index 28d5e18e5b3e752fcfbb43902502097b53b68d2d..53768ab54b556ef2eaed93fdf794fbc0f065ac45 100644 (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>
index b87888f4f4a0ef2793020d512668478196f158e3..2b28492bc1a47f204d8df03d5c876ebcbf5b3269 100644 (file)
@@ -23,6 +23,7 @@ import { PgSummaryPipe } from '../pg-summary.pipe';
 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({
@@ -40,26 +41,61 @@ export class SummaryServiceMock {
 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[] = [
@@ -131,8 +167,6 @@ describe('Dashbord Component', () => {
     }
   ];
 
-  const configValueData: any = 'e90a0d58-658e-4148-8f61-e896c86f0696';
-
   const orchName: any = 'Cephadm';
 
   configureTestBed({
@@ -159,10 +193,9 @@ describe('Dashbord Component', () => {
     );
     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;
@@ -184,45 +217,50 @@ describe('Dashbord Component', () => {
   });
 
   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}`);
   });
@@ -273,21 +311,29 @@ describe('Dashbord Component', () => {
   });
 
   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"]'));
index c4d74c33f3ecce11a90e84d9b8b2ee1544df6db3..ec77518371edf18bcb443bc614c177e3a7366873 100644 (file)
@@ -11,7 +11,12 @@ import {
   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';
@@ -29,40 +34,35 @@ import { AlertClass } from '~/app/shared/enum/health-icon.enum';
 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
@@ -70,9 +70,7 @@ export class DashboardV3Component extends PrometheusListHelper implements OnInit
   borderClass: string;
   alertType: string;
   alertClass = AlertClass;
-  healthData: any;
-  categoryPgAmount: Record<string, number> = {};
-  totalPgs = 0;
+
   queriesResults: { [key: string]: [] } = {
     USEDCAPACITY: [],
     IPS: [],
@@ -85,27 +83,28 @@ export class DashboardV3Component extends PrometheusListHelper implements OnInit
     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(
@@ -144,14 +143,6 @@ export class DashboardV3Component extends PrometheusListHelper implements OnInit
     }
 
     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();
@@ -161,11 +152,12 @@ export class DashboardV3Component extends PrometheusListHelper implements OnInit
 
   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();
@@ -178,9 +170,6 @@ export class DashboardV3Component extends PrometheusListHelper implements OnInit
   }
 
   getDetailsCardData() {
-    this.healthService.getClusterFsid().subscribe((data: string) => {
-      this.detailsCardData.fsid = data;
-    });
     this.orchestratorService.getName().subscribe((data: string) => {
       this.detailsCardData.orchestrator = data;
     });
@@ -251,19 +240,66 @@ export class DashboardV3Component extends PrometheusListHelper implements OnInit
     );
   }
 
+  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();
       }
     });
index b467167fdce06e79c9afdc0b645d577221882d71..eb813a3fe53eb221cc4378344276a9ed91288b5a 100644 (file)
@@ -4,7 +4,7 @@ import { configureTestBed } from '~/testing/unit-test-helper';
 import { PgCategoryService } from '../shared/pg-category.service';
 import { PgSummaryPipe } from './pg-summary.pipe';
 
-describe('OsdSummaryPipe', () => {
+describe('PgSummaryPipe', () => {
   let pipe: PgSummaryPipe;
 
   configureTestBed({
@@ -21,16 +21,19 @@ describe('OsdSummaryPipe', () => {
 
   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
     });
   });
 });
index c12193accd65fbbce33c90d2864348a3ce617da3..62a4e0030999b63575af7147e3eb3f42f6131971 100644 (file)
@@ -1,6 +1,6 @@
 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'
@@ -11,18 +11,16 @@ export class PgSummaryPipe implements PipeTransform {
   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
     };
   }
 }
index 5dde6c100349085ed679160ed2a1007d488ea667..33a156f048f2099f3e7823917ef2c9478d33977e 100644 (file)
@@ -1,6 +1,8 @@
 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';
 
@@ -11,7 +13,7 @@ import { SmartListComponent } from './smart-list/smart-list.component';
 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]
 })
index 9e9ff96e5f830040e5ed6b930e182339d570b2a4..cd1105e1284c4bf3b6aa04615ba2e0cb41f23034 100644 (file)
@@ -1,28 +1,12 @@
-<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>
index 43633dc37895bec98065a390ec8825159ecff254..d3ab1dccedbccf5d96c5cdc0f81a41aa8a9b5252 100644 (file)
@@ -2,7 +2,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
 
 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';
 
@@ -25,28 +24,4 @@ describe('HealthChecksComponent', () => {
   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  '
-    );
-  });
 });
index 84eeac0f31a835f8ce2ba4dbaa4ac9d3c0dcca41..4084aee9986b38dfd82746bdbbac165c5e9b494d 100644 (file)
@@ -37,4 +37,28 @@ describe('HealthService', () => {
     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');
+  });
 });
index b04a27b644d50c055ff62b4d02b177825f1caa24..a7919789c926741fc7e785fd202425fe6b24b9a9 100644 (file)
@@ -1,5 +1,9 @@
 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'
@@ -8,26 +12,26 @@ export class HealthService {
   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`);
   }
 }
index d021f19eba733ff18258e52bd91f2f3128fb0911..87e3b2f3601dbe4f72e94dba8dac8a040d40aa6a 100644 (file)
@@ -3,3 +3,17 @@ export interface DashboardDetails {
   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;
+}
index 22963b5de54ceedb13500295d34499163bfd1bed..c784b06685e1675e0ec4816b0cfe8118d060a34c 100644 (file)
@@ -1,49 +1,59 @@
-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;
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.spec.ts
deleted file mode 100644 (file)
index 4081aa2..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-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);
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/mds-summary.pipe.ts
deleted file mode 100644 (file)
index 43004fa..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-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;
-  }
-}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.spec.ts
deleted file mode 100644 (file)
index 88e457d..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-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
-    });
-  });
-});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/osd-summary.pipe.ts
deleted file mode 100644 (file)
index cf4ea64..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-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;
-  }
-}
index bab30d054236f77544b454616a2ca5c2bf479910..11e83887298310c9a15a302d08b2414ce9a8b51e 100755 (executable)
@@ -22,12 +22,10 @@ import { IscsiBackstorePipe } from './iscsi-backstore.pipe';
 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';
@@ -78,8 +76,6 @@ import { DimlessBinaryPerMinutePipe } from './dimless-binary-per-minute.pipe';
     SearchHighlightPipe,
     HealthIconPipe,
     MgrSummaryPipe,
-    MdsSummaryPipe,
-    OsdSummaryPipe,
     OctalToHumanReadablePipe,
     PathPipe,
     PluralizePipe,
@@ -121,8 +117,6 @@ import { DimlessBinaryPerMinutePipe } from './dimless-binary-per-minute.pipe';
     SearchHighlightPipe,
     HealthIconPipe,
     MgrSummaryPipe,
-    MdsSummaryPipe,
-    OsdSummaryPipe,
     OctalToHumanReadablePipe,
     PathPipe,
     PluralizePipe,
@@ -159,8 +153,6 @@ import { DimlessBinaryPerMinutePipe } from './dimless-binary-per-minute.pipe';
     SanitizeHtmlPipe,
     HealthIconPipe,
     MgrSummaryPipe,
-    MdsSummaryPipe,
-    OsdSummaryPipe,
     OctalToHumanReadablePipe,
     MbpersecondPipe,
     DimlessBinaryPerMinutePipe
index 4a2715e4bf0207df716086b1a4a76cbbda5f5d93..dc888c7dc8eea6d390fe3e845e89dd8b4a0eec44 100755 (executable)
@@ -4976,6 +4976,7 @@ paths:
             trace.
       security:
       - jwt: []
+      summary: Get Cluster's detailed health report
       tags:
       - Health
   /api/health/get_cluster_capacity:
@@ -5427,7 +5428,200 @@ paths:
             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: