From 439fec829fc6290b7f67f725228a974c0728b255 Mon Sep 17 00:00:00 2001 From: alfonsomthd Date: Thu, 30 May 2019 09:57:54 +0200 Subject: [PATCH] mgr/dashboard: show degraded/misplaced/unfound objects. MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit * Landing Page 'Objects' card now is a chart that shows more info about objects. * Fix: Dimless/dimlessBinary pipe applied to amount displayed in chart slice tooltip body (if shown). * Refactoring: simplified way of setting chart initial config via 'config' @Input; erased redundant @Inputs. Updated chart component default config (for the sake of simplicity). Fixes: https://tracker.ceph.com/issues/39613 Signed-off-by: Alfonso Martínez --- qa/tasks/mgr/dashboard/test_health.py | 24 +-- .../mgr/dashboard/controllers/health.py | 9 +- .../health-pie/health-pie.component.spec.ts | 48 +++--- .../health-pie/health-pie.component.ts | 37 ++--- .../dashboard/health/health.component.html | 19 +-- .../dashboard/health/health.component.spec.ts | 36 ++-- .../ceph/dashboard/health/health.component.ts | 155 ++++++++++++++---- .../mgr/dashboard/services/ceph_service.py | 11 +- 8 files changed, 208 insertions(+), 131 deletions(-) diff --git a/qa/tasks/mgr/dashboard/test_health.py b/qa/tasks/mgr/dashboard/test_health.py index 71301cb5dbad9..8e47a280c5bab 100644 --- a/qa/tasks/mgr/dashboard/test_health.py +++ b/qa/tasks/mgr/dashboard/test_health.py @@ -7,6 +7,18 @@ from .helper import DashboardTestCase, JAny, JLeaf, JList, JObj class HealthTest(DashboardTestCase): CEPHFS = True + __pg_info_schema = JObj({ + 'object_stats': JObj({ + 'num_objects': int, + 'num_object_copies': int, + 'num_objects_degraded': int, + 'num_objects_misplaced': int, + 'num_objects_unfound': int + }), + 'pgs_per_osd': float, + 'statuses': JObj({}, allow_unknown=True, unknown_schema=int) + }) + def test_minimal_health(self): data = self._get('/api/health/minimal') self.assertStatus(200) @@ -22,7 +34,6 @@ class HealthTest(DashboardTestCase): 'stats': JObj({ 'total_avail_bytes': int, 'total_bytes': int, - 'total_objects': int, 'total_used_raw_bytes': int, }) }), @@ -65,10 +76,7 @@ class HealthTest(DashboardTestCase): 'up': int, })), }), - 'pg_info': JObj({ - 'pgs_per_osd': float, - 'statuses': JObj({}, allow_unknown=True, unknown_schema=int) - }), + 'pg_info': self.__pg_info_schema, 'pools': JList(JLeaf(dict)), 'rgw': int, 'scrub_status': str @@ -134,7 +142,6 @@ class HealthTest(DashboardTestCase): 'stats': JObj({ 'total_avail_bytes': int, 'total_bytes': int, - 'total_objects': int, 'total_used_bytes': int, 'total_used_raw_bytes': int, 'total_used_raw_ratio': float @@ -243,10 +250,7 @@ class HealthTest(DashboardTestCase): 'up': int, }, allow_unknown=True)), }, allow_unknown=True), - 'pg_info': JObj({ - 'pgs_per_osd': float, - 'statuses': JObj({}, allow_unknown=True, unknown_schema=int) - }), + 'pg_info': self.__pg_info_schema, 'pools': JList(JLeaf(dict)), 'rgw': int, 'scrub_status': str diff --git a/src/pybind/mgr/dashboard/controllers/health.py b/src/pybind/mgr/dashboard/controllers/health.py index eaff5be7173ef..d2b232a6afa42 100644 --- a/src/pybind/mgr/dashboard/controllers/health.py +++ b/src/pybind/mgr/dashboard/controllers/health.py @@ -94,12 +94,10 @@ class HealthData(object): del df['stats_by_class'] - df['stats']['total_objects'] = sum( - [p['stats']['objects'] for p in df['pools']]) if self._minimal: df = dict(stats=self._partial_dict( df['stats'], - ['total_avail_bytes', 'total_bytes', 'total_objects', + ['total_avail_bytes', 'total_bytes', 'total_used_raw_bytes'] )) return df @@ -163,10 +161,7 @@ class HealthData(object): return osd_map def pg_info(self): - pg_info = CephService.get_pg_info() - if self._minimal: - pg_info = self._partial_dict(pg_info, ['pgs_per_osd', 'statuses']) - return pg_info + return CephService.get_pg_info() def pools(self): pools = CephService.get_pool_list_with_stats() diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts index 88f7be522911a..25782502ca55b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.spec.ts @@ -3,6 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { configureTestBed } from '../../../../testing/unit-test-helper'; import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe'; +import { DimlessPipe } from '../../../shared/pipes/dimless.pipe'; import { FormatterService } from '../../../shared/services/formatter.service'; import { HealthPieComponent } from './health-pie.component'; @@ -13,7 +14,7 @@ describe('HealthPieComponent', () => { configureTestBed({ schemas: [NO_ERRORS_SCHEMA], declarations: [HealthPieComponent], - providers: [DimlessBinaryPipe, FormatterService] + providers: [DimlessBinaryPipe, DimlessPipe, FormatterService] }); beforeEach(() => { @@ -25,34 +26,6 @@ describe('HealthPieComponent', () => { expect(component).toBeTruthy(); }); - it('Set doughnut if nothing received', () => { - component.chartType = ''; - fixture.detectChanges(); - - expect(component.chartConfig.chartType).toEqual('doughnut'); - }); - - it('Set doughnut if not allowed value received', () => { - component.chartType = 'badType'; - fixture.detectChanges(); - - expect(component.chartConfig.chartType).toEqual('doughnut'); - }); - - it('Set doughnut if doughnut received', () => { - component.chartType = 'doughnut'; - fixture.detectChanges(); - - expect(component.chartConfig.chartType).toEqual('doughnut'); - }); - - it('Set pie if pie received', () => { - component.chartType = 'pie'; - fixture.detectChanges(); - - expect(component.chartConfig.chartType).toEqual('pie'); - }); - it('Add slice border if there is more than one slice with numeric non zero value', () => { component.chartConfig.dataset[0].data = [48, 0, 1, 0]; component.ngOnChanges(); @@ -81,4 +54,21 @@ describe('HealthPieComponent', () => { expect(component.chartConfig.dataset[0].data).toEqual(initialData); }); + + describe('tooltip body', () => { + const tooltipBody = ['text: 10000']; + + it('should return amount converted to appropriate units', () => { + component.isBytesData = false; + expect(component['getChartTooltipBody'](tooltipBody)).toEqual('text: 10 k'); + + component.isBytesData = true; + expect(component['getChartTooltipBody'](tooltipBody)).toEqual('text: 9.8 KiB'); + }); + + it('should not return amount when showing label as tooltip', () => { + component.showLabelAsTooltip = true; + expect(component['getChartTooltipBody'](tooltipBody)).toEqual('text'); + }); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts index 1016254c56285..f4baefb494b37 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health-pie/health-pie.component.ts @@ -14,6 +14,7 @@ import * as _ from 'lodash'; import { ChartTooltip } from '../../../shared/models/chart-tooltip'; import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe'; +import { DimlessPipe } from '../../../shared/pipes/dimless.pipe'; import { HealthPieColor } from './health-pie-color.enum'; @Component({ @@ -30,12 +31,10 @@ export class HealthPieComponent implements OnChanges, OnInit { @Input() data: any; @Input() - chartType: string; + config = {}; @Input() isBytesData = false; @Input() - displayLegend = false; - @Input() tooltipFn: any; @Input() showLabelAsTooltip = false; @@ -43,6 +42,7 @@ export class HealthPieComponent implements OnChanges, OnInit { prepareFn = new EventEmitter(); chartConfig: any = { + chartType: 'pie', dataset: [ { label: null, @@ -51,7 +51,7 @@ export class HealthPieComponent implements OnChanges, OnInit { ], options: { legend: { - display: false, + display: true, position: 'right', labels: { usePointStyle: true }, onClick: (event, legendItem) => { @@ -59,15 +59,17 @@ export class HealthPieComponent implements OnChanges, OnInit { } }, animation: { duration: 0 }, - tooltips: { enabled: false + }, + title: { + display: false } } }; private hiddenSlices = []; - constructor(private dimlessBinary: DimlessBinaryPipe) {} + constructor(private dimlessBinary: DimlessBinaryPipe, private dimless: DimlessPipe) {} ngOnInit() { // An extension to Chart.js to enable rendering some @@ -121,10 +123,6 @@ export class HealthPieComponent implements OnChanges, OnInit { chartTooltip.customTooltips(tooltip); }; - this.setChartType(); - - this.chartConfig.options.legend.display = this.displayLegend; - this.chartConfig.colors = [ { backgroundColor: [ @@ -137,6 +135,8 @@ export class HealthPieComponent implements OnChanges, OnInit { } ]; + _.merge(this.chartConfig, this.config); + this.prepareFn.emit([this.chartConfig, this.data]); } @@ -153,24 +153,13 @@ export class HealthPieComponent implements OnChanges, OnInit { return bodySplit[0]; } - if (this.isBytesData) { - bodySplit[1] = this.dimlessBinary.transform(bodySplit[1]); - } + bodySplit[1] = this.isBytesData + ? this.dimlessBinary.transform(bodySplit[1]) + : this.dimless.transform(bodySplit[1]); return bodySplit.join(': '); } - private setChartType() { - const chartTypes = ['doughnut', 'pie']; - const selectedChartType = chartTypes.find((chartType) => chartType === this.chartType); - - if (selectedChartType !== undefined) { - this.chartConfig.chartType = selectedChartType; - } else { - this.chartConfig.chartType = chartTypes[0]; - } - } - private setChartSliceBorderWidth() { let nonZeroValueSlices = 0; _.forEach(this.chartConfig.dataset[0].data, function(slice) { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.html index 592c14b8d8c46..a2f7244b619c6 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.html @@ -148,9 +148,6 @@ *ngIf="healthData.client_perf"> @@ -183,7 +180,6 @@ class="row info-group" *ngIf="healthData.pools || healthData.df - || healthData.df?.stats?.total_objects != null || healthData.pg_info">
@@ -204,9 +200,8 @@ contentClass="content-chart" *ngIf="healthData.df"> @@ -215,9 +210,12 @@ i18n-cardTitle class="cd-col-5" cardClass="card-medium" - contentClass="content-medium content-highlight" - *ngIf="healthData.df?.stats?.total_objects != null"> - {{ healthData.df?.stats?.total_objects }} + contentClass="content-chart" + *ngIf="healthData.pg_info?.object_stats?.num_objects != null"> + +
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.spec.ts index b4823fc8485d3..8c89000ca37d7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.spec.ts @@ -15,7 +15,6 @@ import { FeatureTogglesService } from '../../../shared/services/feature-toggles. import { RefreshIntervalService } from '../../../shared/services/refresh-interval.service'; import { SharedModule } from '../../../shared/shared.module'; import { PgCategoryService } from '../../shared/pg-category.service'; -import { HealthPieColor } from '../health-pie/health-pie-color.enum'; import { HealthPieComponent } from '../health-pie/health-pie.component'; import { MdsSummaryPipe } from '../mds-summary.pipe'; import { MgrSummaryPipe } from '../mgr-summary.pipe'; @@ -39,8 +38,8 @@ describe('HealthComponent', () => { client_perf: {}, scrub_status: 'Inactive', pools: [], - df: { stats: { total_objects: 0 } }, - pg_info: {} + df: { stats: {} }, + pg_info: { object_stats: { num_objects: 0 } } }; const fakeAuthStorageService = { getPermissions: () => { @@ -248,20 +247,18 @@ describe('HealthComponent', () => { expect(preparePgStatus).toHaveBeenCalled(); }); + it('event binding "prepareObjects" is called', () => { + const prepareObjects = spyOn(component, 'prepareObjects'); + + fixture.detectChanges(); + + expect(prepareObjects).toHaveBeenCalled(); + }); + describe('preparePgStatus', () => { const calcPercentage = (data) => Math.round((data / 10) * 100) || 0; const expectedChart = (data: number[]) => ({ - colors: [ - { - backgroundColor: [ - HealthPieColor.DEFAULT_GREEN, - HealthPieColor.DEFAULT_BLUE, - HealthPieColor.DEFAULT_ORANGE, - HealthPieColor.DEFAULT_RED - ] - } - ], labels: [ `Clean (${calcPercentage(data[0])}%)`, `Working (${calcPercentage(data[1])}%)`, @@ -326,4 +323,17 @@ describe('HealthComponent', () => { expect(component.isClientReadWriteChartShowable()).toBeTruthy(); }); }); + + describe('calcPercentage', () => { + it('returns correct value', () => { + expect(component['calcPercentage'](1, undefined)).toEqual(0); + expect(component['calcPercentage'](1, null)).toEqual(0); + expect(component['calcPercentage'](1, 0)).toEqual(0); + expect(component['calcPercentage'](undefined, 1)).toEqual(0); + expect(component['calcPercentage'](null, 1)).toEqual(0); + expect(component['calcPercentage'](0, 1)).toEqual(0); + expect(component['calcPercentage'](2.346, 10)).toEqual(23); + expect(component['calcPercentage'](2.35, 10)).toEqual(24); + }); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.ts index d43024bdcfd33..6cca3b5cac495 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/health/health.component.ts @@ -7,6 +7,7 @@ import { Subscription } from 'rxjs/Subscription'; import { HealthService } from '../../../shared/api/health.service'; import { Permissions } from '../../../shared/models/permissions'; import { DimlessBinaryPipe } from '../../../shared/pipes/dimless-binary.pipe'; +import { DimlessPipe } from '../../../shared/pipes/dimless.pipe'; import { AuthStorageService } from '../../../shared/services/auth-storage.service'; import { FeatureTogglesMap$, @@ -27,6 +28,39 @@ export class HealthComponent implements OnInit, OnDestroy { permissions: Permissions; enabledFeature$: FeatureTogglesMap$; + rawCapacityChartConfig = { + options: { + title: { display: true, position: 'bottom' } + } + }; + objectsChartConfig = { + options: { + title: { display: true, position: 'bottom' } + }, + colors: [ + { + backgroundColor: [ + HealthPieColor.DEFAULT_GREEN, + HealthPieColor.DEFAULT_MAGENTA, + HealthPieColor.DEFAULT_ORANGE, + HealthPieColor.DEFAULT_RED + ] + } + ] + }; + pgStatusChartConfig = { + colors: [ + { + backgroundColor: [ + HealthPieColor.DEFAULT_GREEN, + HealthPieColor.DEFAULT_BLUE, + HealthPieColor.DEFAULT_ORANGE, + HealthPieColor.DEFAULT_RED + ] + } + ] + }; + constructor( private healthService: HealthService, private i18n: I18n, @@ -34,7 +68,8 @@ export class HealthComponent implements OnInit, OnDestroy { private pgCategoryService: PgCategoryService, private featureToggles: FeatureTogglesService, private refreshIntervalService: RefreshIntervalService, - private dimlessBinary: DimlessBinaryPipe + private dimlessBinary: DimlessBinaryPipe, + private dimless: DimlessPipe ) { this.permissions = this.authStorageService.getPermissions(); this.enabledFeature$ = this.featureToggles.get(); @@ -63,12 +98,20 @@ export class HealthComponent implements OnInit, OnDestroy { const total = this.healthData.client_perf.write_op_per_sec + this.healthData.client_perf.read_op_per_sec; - const calcPercentage = (status) => - Math.round(((this.healthData.client_perf[status] || 0) / total) * 100); - ratioLabels.push(`${this.i18n('Writes')} (${calcPercentage('write_op_per_sec')}%)`); + ratioLabels.push( + `${this.i18n('Writes')} (${this.calcPercentage( + this.healthData.client_perf.write_op_per_sec, + total + )}%)` + ); ratioData.push(this.healthData.client_perf.write_op_per_sec); - ratioLabels.push(`${this.i18n('Reads')} (${calcPercentage('read_op_per_sec')}%)`); + ratioLabels.push( + `${this.i18n('Reads')} (${this.calcPercentage( + this.healthData.client_perf.read_op_per_sec, + total + )}%)` + ); ratioData.push(this.healthData.client_perf.read_op_per_sec); chart.dataset[0].data = ratioData; @@ -76,20 +119,17 @@ export class HealthComponent implements OnInit, OnDestroy { } prepareRawUsage(chart, data) { - const percentAvailable = Math.round( - 100 * - ((data.df.stats.total_bytes - data.df.stats.total_used_raw_bytes) / - data.df.stats.total_bytes) + const percentAvailable = this.calcPercentage( + data.df.stats.total_bytes - data.df.stats.total_used_raw_bytes, + data.df.stats.total_bytes ); - - const percentUsed = Math.round( - 100 * (data.df.stats.total_used_raw_bytes / data.df.stats.total_bytes) + const percentUsed = this.calcPercentage( + data.df.stats.total_used_raw_bytes, + data.df.stats.total_bytes ); chart.dataset[0].data = [data.df.stats.total_used_raw_bytes, data.df.stats.total_avail_bytes]; - if (chart === 'doughnut') { - chart.options.cutoutPercentage = 65; - } + chart.labels = [ `${this.dimlessBinary.transform(data.df.stats.total_used_raw_bytes)} ${this.i18n( 'Used' @@ -99,25 +139,13 @@ export class HealthComponent implements OnInit, OnDestroy { )} ${this.i18n('Avail.')} (${percentAvailable}%)` ]; - chart.options.title = { - display: true, - text: `${this.dimlessBinary.transform(data.df.stats.total_bytes)} total`, - position: 'bottom' - }; + chart.options.title.text = `${this.dimlessBinary.transform( + data.df.stats.total_bytes + )} ${this.i18n('total')}`; } preparePgStatus(chart, data) { const categoryPgAmount = {}; - chart.colors = [ - { - backgroundColor: [ - HealthPieColor.DEFAULT_GREEN, - HealthPieColor.DEFAULT_BLUE, - HealthPieColor.DEFAULT_ORANGE, - HealthPieColor.DEFAULT_RED - ] - } - ]; _.forEach(data.pg_info.statuses, (pgAmount, pgStatesText) => { const categoryType = this.pgCategoryService.getTypeByStates(pgStatesText); @@ -132,15 +160,62 @@ export class HealthComponent implements OnInit, OnDestroy { .getAllTypes() .map((categoryType) => categoryPgAmount[categoryType]); - const calcPercentage = (status) => - Math.round(((categoryPgAmount[status] || 0) / data.pg_info.pgs_per_osd) * 100) || 0; + chart.labels = [ + `${this.i18n('Clean')} (${this.calcPercentage( + categoryPgAmount['clean'], + data.pg_info.pgs_per_osd + )}%)`, + `${this.i18n('Working')} (${this.calcPercentage( + categoryPgAmount['working'], + data.pg_info.pgs_per_osd + )}%)`, + `${this.i18n('Warning')} (${this.calcPercentage( + categoryPgAmount['warning'], + data.pg_info.pgs_per_osd + )}%)`, + `${this.i18n('Unknown')} (${this.calcPercentage( + categoryPgAmount['unknown'], + data.pg_info.pgs_per_osd + )}%)` + ]; + } + + prepareObjects(chart, data) { + const totalReplicas = data.pg_info.object_stats.num_object_copies; + const healthy = + totalReplicas - + data.pg_info.object_stats.num_objects_misplaced - + data.pg_info.object_stats.num_objects_degraded - + data.pg_info.object_stats.num_objects_unfound; chart.labels = [ - `${this.i18n('Clean')} (${calcPercentage('clean')}%)`, - `${this.i18n('Working')} (${calcPercentage('working')}%)`, - `${this.i18n('Warning')} (${calcPercentage('warning')}%)`, - `${this.i18n('Unknown')} (${calcPercentage('unknown')}%)` + `${this.i18n('Healthy')} (${this.calcPercentage(healthy, totalReplicas)}%)`, + `${this.i18n('Misplaced')} (${this.calcPercentage( + data.pg_info.object_stats.num_objects_misplaced, + totalReplicas + )}%)`, + `${this.i18n('Degraded')} (${this.calcPercentage( + data.pg_info.object_stats.num_objects_degraded, + totalReplicas + )}%)`, + `${this.i18n('Unfound')} (${this.calcPercentage( + data.pg_info.object_stats.num_objects_unfound, + totalReplicas + )}%)` ]; + + chart.dataset[0].data = [ + healthy, + data.pg_info.object_stats.num_objects_misplaced, + data.pg_info.object_stats.num_objects_degraded, + data.pg_info.object_stats.num_objects_unfound + ]; + + chart.options.title.text = `${this.dimless.transform( + data.pg_info.object_stats.num_objects + )} ${this.i18n('total')} (${this.dimless.transform(totalReplicas)} ${this.i18n('replicas')})`; + + chart.options.maintainAspectRatio = window.innerWidth >= 375; } isClientReadWriteChartShowable() { @@ -149,4 +224,12 @@ export class HealthComponent implements OnInit, OnDestroy { return readOps + writeOps > 0; } + + private calcPercentage(dividend: number, divisor: number) { + if (!_.isNumber(dividend) || !_.isNumber(divisor) || divisor === 0) { + return 0; + } + + return Math.round((dividend / divisor) * 100); + } } diff --git a/src/pybind/mgr/dashboard/services/ceph_service.py b/src/pybind/mgr/dashboard/services/ceph_service.py index a1905afbfd803..4e14dd84fb6ff 100644 --- a/src/pybind/mgr/dashboard/services/ceph_service.py +++ b/src/pybind/mgr/dashboard/services/ceph_service.py @@ -18,6 +18,11 @@ except ImportError: from .. import logger, mgr +try: + from typing import Dict, Any # pylint: disable=unused-import +except ImportError: + pass # For typing only + class SendCommandError(rados.Error): def __init__(self, err, prefix, argdict, errno): @@ -40,7 +45,7 @@ class CephService(object): @classmethod def get_service_map(cls, service_name): - service_map = {} + service_map = {} # type: Dict[str, Dict[str, Any]] for server in mgr.list_servers(): for service in server['services']: if service['type'] == service_name: @@ -234,6 +239,9 @@ class CephService(object): @classmethod def get_pg_info(cls): pg_summary = mgr.get('pg_summary') + object_stats = {stat: pg_summary['pg_stats_sum']['stat_sum'][stat] for stat in [ + 'num_objects', 'num_object_copies', 'num_objects_degraded', + 'num_objects_misplaced', 'num_objects_unfound']} pgs_per_osd = 0.0 total_osds = len(pg_summary['by_osd']) @@ -246,6 +254,7 @@ class CephService(object): pgs_per_osd = total_pgs / total_osds return { + 'object_stats': object_stats, 'statuses': pg_summary['all'], 'pgs_per_osd': pgs_per_osd, } -- 2.39.5