From c588269e912ce6a81d0597fe89cb2c7b7086225f Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Thu, 28 Mar 2019 22:49:44 +0000 Subject: [PATCH] mgr/dashboard: Improve iSCSI overview page iSCSI overview page will now use information obtained from ceph-iscsi. Fixes: https://tracker.ceph.com/issues/39024 Signed-off-by: Ricardo Marques --- .../mgr/dashboard/controllers/health.py | 5 +- src/pybind/mgr/dashboard/controllers/iscsi.py | 61 ++++++++++ .../mgr/dashboard/controllers/tcmu_iscsi.py | 13 --- .../app/ceph/block/iscsi/iscsi.component.html | 49 +++++++- .../ceph/block/iscsi/iscsi.component.spec.ts | 12 +- .../app/ceph/block/iscsi/iscsi.component.ts | 88 +++++++------- .../src/app/shared/api/iscsi.service.ts | 4 + .../app/shared/api/tcmu-iscsi.service.spec.ts | 40 ------- .../src/app/shared/api/tcmu-iscsi.service.ts | 15 --- .../frontend/src/locale/messages.xlf | 67 ++++------- .../mgr/dashboard/services/tcmu_service.py | 15 +-- .../mgr/dashboard/tests/test_tcmu_iscsi.py | 109 ------------------ 12 files changed, 191 insertions(+), 287 deletions(-) delete mode 100644 src/pybind/mgr/dashboard/controllers/tcmu_iscsi.py delete mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/tcmu-iscsi.service.spec.ts delete mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/api/tcmu-iscsi.service.ts delete mode 100644 src/pybind/mgr/dashboard/tests/test_tcmu_iscsi.py diff --git a/src/pybind/mgr/dashboard/controllers/health.py b/src/pybind/mgr/dashboard/controllers/health.py index e3e3fc5939a..eaff5be7173 100644 --- a/src/pybind/mgr/dashboard/controllers/health.py +++ b/src/pybind/mgr/dashboard/controllers/health.py @@ -8,7 +8,7 @@ from . import ApiController, Endpoint, BaseController from .. import mgr from ..security import Permission, Scope from ..services.ceph_service import CephService -from ..services.tcmu_service import TcmuService +from ..services.iscsi_cli import IscsiGatewaysConfig class HealthData(object): @@ -123,7 +123,8 @@ class HealthData(object): return len(mgr.list_servers()) def iscsi_daemons(self): - return TcmuService.get_iscsi_daemons_amount() + gateways = IscsiGatewaysConfig.get_gateways_config()['gateways'] + return len(gateways) if gateways else 0 def mgr_map(self): mgr_map = mgr.get('mgr_map') diff --git a/src/pybind/mgr/dashboard/controllers/iscsi.py b/src/pybind/mgr/dashboard/controllers/iscsi.py index 5990e64c118..36a0823b757 100644 --- a/src/pybind/mgr/dashboard/controllers/iscsi.py +++ b/src/pybind/mgr/dashboard/controllers/iscsi.py @@ -17,6 +17,7 @@ from ..security import Scope from ..services.iscsi_client import IscsiClient from ..services.iscsi_cli import IscsiGatewaysConfig from ..services.rbd import format_bitmask +from ..services.tcmu_service import TcmuService from ..exceptions import DashboardException from ..tools import TaskManager @@ -71,6 +72,66 @@ class IscsiUi(BaseController): portals.append({'name': name, 'ip_addresses': ip_addresses['data']}) return sorted(portals, key=lambda p: '{}.{}'.format(p['name'], p['ip_addresses'])) + @Endpoint() + @ReadPermission + def overview(self): + result_gateways = [] + result_images = [] + gateways_names = IscsiGatewaysConfig.get_gateways_config()['gateways'].keys() + config = None + for gateway_name in gateways_names: + try: + config = IscsiClient.instance(gateway_name=gateway_name).get_config() + break + except RequestException: + pass + + # Gateways info + for gateway_name in gateways_names: + gateway = { + 'name': gateway_name, + 'state': 'N/A', + 'num_targets': 'N/A' + } + try: + IscsiClient.instance(gateway_name=gateway_name).ping() + gateway['state'] = 'up' + except RequestException: + gateway['state'] = 'down' + if config: + gateway['num_targets'] = len([target for _, target in config['targets'].items() + if gateway_name in target['portals']]) + result_gateways.append(gateway) + + # Images info + if config: + tcmu_info = TcmuService.get_iscsi_info() + for _, disk_config in config['disks'].items(): + image = { + 'pool': disk_config['pool'], + 'image': disk_config['image'], + 'backstore': disk_config['backstore'], + 'optimized_since': None, + 'stats': None, + 'stats_history': None + } + tcmu_image_info = TcmuService.get_image_info(image['pool'], + image['image'], + tcmu_info) + if tcmu_image_info: + if 'optimized_since' in tcmu_image_info: + image['optimized_since'] = tcmu_image_info['optimized_since'] + if 'stats' in tcmu_image_info: + image['stats'] = tcmu_image_info['stats'] + if 'stats_history' in tcmu_image_info: + image['stats_history'] = tcmu_image_info['stats_history'] + result_images.append(image) + + return { + 'gateways': sorted(result_gateways, key=lambda g: g['name']), + 'images': sorted(result_images, key=lambda i: '{}/{}'.format(i['pool'], i['image'])) + } + @ApiController('/iscsi', Scope.ISCSI) class Iscsi(BaseController): diff --git a/src/pybind/mgr/dashboard/controllers/tcmu_iscsi.py b/src/pybind/mgr/dashboard/controllers/tcmu_iscsi.py deleted file mode 100644 index 1da816facb1..00000000000 --- a/src/pybind/mgr/dashboard/controllers/tcmu_iscsi.py +++ /dev/null @@ -1,13 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - -from . import ApiController, RESTController -from ..security import Scope -from ..services.tcmu_service import TcmuService - - -@ApiController('/tcmuiscsi', Scope.ISCSI) -class TcmuIscsi(RESTController): - - def list(self): - return TcmuService.get_iscsi_info() diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.html index 7cbfcbf6f37..59604a24383 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.html @@ -1,12 +1,55 @@ -Daemons -Gateways + + [columns]="gatewaysColumns"> Images + + + {{ value }} + + + + + + + + n/a + + + + + + {{ value }} /s + + + n/a + + + + + + {{ value | relativeDate }} + + + n/a + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts index dcc1be8a1a0..e9bae7f3213 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts @@ -4,7 +4,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; import { configureTestBed, i18nProviders } from '../../../../testing/unit-test-helper'; -import { TcmuIscsiService } from '../../../shared/api/tcmu-iscsi.service'; +import { IscsiService } from '../../../shared/api/iscsi.service'; import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe'; import { DimlessPipe } from '../../../shared/pipes/dimless.pipe'; import { ListPipe } from '../../../shared/pipes/list.pipe'; @@ -15,11 +15,11 @@ import { IscsiComponent } from './iscsi.component'; describe('IscsiComponent', () => { let component: IscsiComponent; let fixture: ComponentFixture; - let tcmuIscsiService: TcmuIscsiService; + let iscsiService: IscsiService; let tcmuiscsiData; const fakeService = { - tcmuiscsi: () => { + overview: () => { return new Promise(function() { return; }); @@ -36,7 +36,7 @@ describe('IscsiComponent', () => { FormatterService, RelativeDatePipe, ListPipe, - { provide: TcmuIscsiService, useValue: fakeService }, + { provide: IscsiService, useValue: fakeService }, i18nProviders ] }); @@ -44,12 +44,12 @@ describe('IscsiComponent', () => { beforeEach(() => { fixture = TestBed.createComponent(IscsiComponent); component = fixture.componentInstance; - tcmuIscsiService = TestBed.get(TcmuIscsiService); + iscsiService = TestBed.get(IscsiService); fixture.detectChanges(); tcmuiscsiData = { images: [] }; - spyOn(tcmuIscsiService, 'tcmuiscsi').and.callFake(() => of(tcmuiscsiData)); + spyOn(iscsiService, 'overview').and.callFake(() => of(tcmuiscsiData)); }); it('should create', () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.ts index 0b9195ec417..fe64d02c385 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.ts @@ -1,12 +1,9 @@ -import { Component } from '@angular/core'; +import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core'; import { I18n } from '@ngx-translate/i18n-polyfill'; -import { TcmuIscsiService } from '../../../shared/api/tcmu-iscsi.service'; -import { CellTemplate } from '../../../shared/enum/cell-template.enum'; -import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe'; +import { IscsiService } from '../../../shared/api/iscsi.service'; import { DimlessPipe } from '../../../shared/pipes/dimless.pipe'; -import { ListPipe } from '../../../shared/pipes/list.pipe'; import { RelativeDatePipe } from '../../../shared/pipes/relative-date.pipe'; @Component({ @@ -14,92 +11,91 @@ import { RelativeDatePipe } from '../../../shared/pipes/relative-date.pipe'; templateUrl: './iscsi.component.html', styleUrls: ['./iscsi.component.scss'] }) -export class IscsiComponent { - daemons = []; - daemonsColumns: any; +export class IscsiComponent implements OnInit { + @ViewChild('statusColorTpl') + statusColorTpl: TemplateRef; + @ViewChild('iscsiSparklineTpl') + iscsiSparklineTpl: TemplateRef; + @ViewChild('iscsiPerSecondTpl') + iscsiPerSecondTpl: TemplateRef; + @ViewChild('iscsiRelativeDateTpl') + iscsiRelativeDateTpl: TemplateRef; + + gateways = []; + gatewaysColumns: any; images = []; imagesColumns: any; constructor( - private tcmuIscsiService: TcmuIscsiService, - cephShortVersionPipe: CephShortVersionPipe, - dimlessPipe: DimlessPipe, - relativeDatePipe: RelativeDatePipe, - listPipe: ListPipe, + private iscsiService: IscsiService, + private dimlessPipe: DimlessPipe, + private relativeDatePipe: RelativeDatePipe, private i18n: I18n - ) { - this.daemonsColumns = [ - { - name: this.i18n('Hostname'), - prop: 'server_hostname' - }, + ) {} + + ngOnInit() { + this.gatewaysColumns = [ { - name: this.i18n('# Active/Optimized'), - prop: 'optimized_paths' + name: this.i18n('Name'), + prop: 'name' }, { - name: this.i18n('# Active/Non-Optimized'), - prop: 'non_optimized_paths' + name: this.i18n('State'), + prop: 'state', + cellTemplate: this.statusColorTpl }, { - name: this.i18n('Version'), - prop: 'version', - pipe: cephShortVersionPipe + name: this.i18n('# Targets'), + prop: 'num_targets' } ]; this.imagesColumns = [ { name: this.i18n('Pool'), - prop: 'pool_name' + prop: 'pool' }, { name: this.i18n('Image'), - prop: 'name' - }, - { - name: this.i18n('Active/Optimized'), - prop: 'optimized_paths', - pipe: listPipe + prop: 'image' }, { - name: this.i18n('Active/Non-Optimized'), - prop: 'non_optimized_paths', - pipe: listPipe + name: this.i18n('Backstore'), + prop: 'backstore' }, { name: this.i18n('Read Bytes'), prop: 'stats_history.rd_bytes', - cellTransformation: CellTemplate.sparkline + cellTemplate: this.iscsiSparklineTpl }, { name: this.i18n('Write Bytes'), prop: 'stats_history.wr_bytes', - cellTransformation: CellTemplate.sparkline + cellTemplate: this.iscsiSparklineTpl }, { name: this.i18n('Read Ops'), prop: 'stats.rd', - pipe: dimlessPipe, - cellTransformation: CellTemplate.perSecond + pipe: this.dimlessPipe, + cellTemplate: this.iscsiPerSecondTpl }, { name: this.i18n('Write Ops'), prop: 'stats.wr', - pipe: dimlessPipe, - cellTransformation: CellTemplate.perSecond + pipe: this.dimlessPipe, + cellTemplate: this.iscsiPerSecondTpl }, { name: this.i18n('A/O Since'), prop: 'optimized_since', - pipe: relativeDatePipe + cellTemplate: this.iscsiRelativeDateTpl } ]; } refresh() { - this.tcmuIscsiService.tcmuiscsi().subscribe((resp: any) => { - this.daemons = resp.daemons; - this.images = resp.images; + this.iscsiService.overview().subscribe((overview: Array) => { + this.gateways = overview['gateways']; + this.images = overview['images']; this.images.map((image) => { if (image.stats_history) { image.stats_history.rd_bytes = image.stats_history.rd_bytes.map((i) => i[1]); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/iscsi.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/iscsi.service.ts index b829ea95dc6..0c1ec456552 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/iscsi.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/iscsi.service.ts @@ -101,4 +101,8 @@ export class IscsiService { updateDiscovery(auth) { return this.http.put(`api/iscsi/discoveryauth`, auth); } + + overview() { + return this.http.get(`ui-api/iscsi/overview`); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/tcmu-iscsi.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/tcmu-iscsi.service.spec.ts deleted file mode 100644 index 99cd424215d..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/tcmu-iscsi.service.spec.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; - -import { configureTestBed } from '../../../testing/unit-test-helper'; -import { TcmuIscsiService } from './tcmu-iscsi.service'; - -describe('TcmuIscsiService', () => { - let service: TcmuIscsiService; - let httpTesting: HttpTestingController; - - configureTestBed({ - providers: [TcmuIscsiService], - imports: [HttpClientTestingModule] - }); - - beforeEach(() => { - service = TestBed.get(TcmuIscsiService); - httpTesting = TestBed.get(HttpTestingController); - }); - - afterEach(() => { - httpTesting.verify(); - }); - - it('should be created', () => { - expect(service).toBeTruthy(); - }); - - it('should call tcmuiscsi', fakeAsync(() => { - let result; - service.tcmuiscsi().subscribe((resp) => { - result = resp; - }); - const req = httpTesting.expectOne('api/tcmuiscsi'); - expect(req.request.method).toBe('GET'); - req.flush(['foo']); - tick(); - expect(result).toEqual(['foo']); - })); -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/tcmu-iscsi.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/tcmu-iscsi.service.ts deleted file mode 100644 index 24a8e878509..00000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/tcmu-iscsi.service.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { Injectable } from '@angular/core'; - -import { ApiModule } from './api.module'; - -@Injectable({ - providedIn: ApiModule -}) -export class TcmuIscsiService { - constructor(private http: HttpClient) {} - - tcmuiscsi() { - return this.http.get('api/tcmuiscsi'); - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf b/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf index 54cc5273d6d..4ffef753b6b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf +++ b/src/pybind/mgr/dashboard/frontend/src/locale/messages.xlf @@ -194,10 +194,6 @@ app/ceph/block/mirroring/overview/overview.component.html 5 - - app/ceph/block/iscsi/iscsi.component.html - 3 - Users @@ -2329,6 +2325,12 @@ app/ceph/block/rbd-trash-move-modal/rbd-trash-move-modal.component.html 40 + + Gateways + + app/ceph/block/iscsi/iscsi.component.html + 3 + Overview @@ -4245,48 +4247,39 @@ 1 - - Hostname + + State src/app/ceph/block/iscsi/iscsi.component.ts 1 - src/app/ceph/block/mirroring/daemon-list/daemon-list.component.ts + src/app/ceph/block/mirroring/image-list/image-list.component.ts 1 - src/app/ceph/cluster/hosts/hosts.component.ts + src/app/ceph/block/mirroring/image-list/image-list.component.ts 1 - src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts + src/app/ceph/block/mirroring/image-list/image-list.component.ts 1 - - - # Active/Optimized - src/app/ceph/block/iscsi/iscsi.component.ts + src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts 1 - - - # Active/Non-Optimized - src/app/ceph/block/iscsi/iscsi.component.ts + src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.ts 1 - - - Active/Optimized - src/app/ceph/block/iscsi/iscsi.component.ts + src/app/ceph/cluster/prometheus/prometheus-list/prometheus-list.component.ts 1 - - Active/Non-Optimized + + # Targets src/app/ceph/block/iscsi/iscsi.component.ts 1 @@ -4334,39 +4327,27 @@ 1 - - Issue + + Hostname - src/app/ceph/block/mirroring/image-list/image-list.component.ts + src/app/ceph/block/mirroring/daemon-list/daemon-list.component.ts 1 - - - State - src/app/ceph/block/mirroring/image-list/image-list.component.ts + src/app/ceph/cluster/hosts/hosts.component.ts 1 - src/app/ceph/block/mirroring/image-list/image-list.component.ts + src/app/ceph/rgw/rgw-daemon-list/rgw-daemon-list.component.ts 1 + + + Issue src/app/ceph/block/mirroring/image-list/image-list.component.ts 1 - - src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts - 1 - - - src/app/ceph/cephfs/cephfs-detail/cephfs-detail.component.ts - 1 - - - src/app/ceph/cluster/prometheus/prometheus-list/prometheus-list.component.ts - 1 - Progress diff --git a/src/pybind/mgr/dashboard/services/tcmu_service.py b/src/pybind/mgr/dashboard/services/tcmu_service.py index 02f65585808..d28a49543e9 100644 --- a/src/pybind/mgr/dashboard/services/tcmu_service.py +++ b/src/pybind/mgr/dashboard/services/tcmu_service.py @@ -86,13 +86,8 @@ class TcmuService(object): } @staticmethod - def get_iscsi_daemons_amount(): - daemons = {} - for service in CephService.get_service_list(SERVICE_TYPE): - hostname = service['hostname'] - - daemon = daemons.get(hostname, None) - if daemon is None: - daemons[hostname] = True - - return len(daemons) + def get_image_info(pool_name, image_name, get_iscsi_info): + for image in get_iscsi_info['images']: + if image['pool_name'] == pool_name and image['name'] == image_name: + return image + return None diff --git a/src/pybind/mgr/dashboard/tests/test_tcmu_iscsi.py b/src/pybind/mgr/dashboard/tests/test_tcmu_iscsi.py deleted file mode 100644 index cff68f1e38a..00000000000 --- a/src/pybind/mgr/dashboard/tests/test_tcmu_iscsi.py +++ /dev/null @@ -1,109 +0,0 @@ -from __future__ import absolute_import - -from . import ControllerTestCase -from .. import mgr -from ..controllers.tcmu_iscsi import TcmuIscsi - -mocked_servers = [{ - 'ceph_version': 'ceph version 13.0.0-5083- () mimic (dev)', - 'hostname': 'ceph-dev1', - 'services': [{'id': 'ceph-dev1:pool1/image1', 'type': 'tcmu-runner'}] -}, { - 'ceph_version': 'ceph version 13.0.0-5083- () mimic (dev)', - 'hostname': 'ceph-dev2', - 'services': [{'id': 'ceph-dev2:pool1/image1', 'type': 'tcmu-runner'}] -}] - -mocked_metadata1 = { - 'ceph_version': 'ceph version 13.0.0-5083- () mimic (dev)', - 'pool_name': 'pool1', - 'image_name': 'image1', - 'image_id': '42', - 'optimized_since': 1152121348, -} - -mocked_metadata2 = { - 'ceph_version': 'ceph version 13.0.0-5083- () mimic (dev)', - 'pool_name': 'pool1', - 'image_name': 'image1', - 'image_id': '42', - 'optimized_since': 0, -} - -mocked_get_daemon_status = { - 'lock_owner': 'true', -} - -mocked_get_counter1 = { - 'librbd-42-pool1-image1.lock_acquired_time': [[1152121348 * 1000000000, - 1152121348 * 1000000000]], - 'librbd-42-pool1-image1.rd': [[0, 0], [1, 43]], - 'librbd-42-pool1-image1.wr': [[0, 0], [1, 44]], - 'librbd-42-pool1-image1.rd_bytes': [[0, 0], [1, 45]], - 'librbd-42-pool1-image1.wr_bytes': [[0, 0], [1, 46]], -} - -mocked_get_counter2 = { - 'librbd-42-pool1-image1.lock_acquired_time': [[0, 0]], - 'librbd-42-pool1-image1.rd': [], - 'librbd-42-pool1-image1.wr': [], - 'librbd-42-pool1-image1.rd_bytes': [], - 'librbd-42-pool1-image1.wr_bytes': [], -} - - -def _get_counter(_daemon_type, daemon_name, _stat): - if daemon_name == 'ceph-dev1:pool1/image1': - return mocked_get_counter1 - if daemon_name == 'ceph-dev2:pool1/image1': - return mocked_get_counter2 - return Exception('invalid daemon name') - - -class TcmuIscsiControllerTest(ControllerTestCase): - - @classmethod - def setup_server(cls): - mgr.list_servers.return_value = mocked_servers - mgr.get_metadata.side_effect = [mocked_metadata1, mocked_metadata2] - mgr.get_daemon_status.return_value = mocked_get_daemon_status - mgr.get_counter.side_effect = _get_counter - mgr.url_prefix = '' - TcmuIscsi._cp_config['tools.authenticate.on'] = False # pylint: disable=protected-access - - cls.setup_controllers(TcmuIscsi, "/test") - - def test_list(self): - self._get('/test/api/tcmuiscsi') - self.assertStatus(200) - self.assertJsonBody({ - 'daemons': [ - { - 'server_hostname': 'ceph-dev1', - 'version': 'ceph version 13.0.0-5083- () mimic (dev)', - 'optimized_paths': 1, 'non_optimized_paths': 0}, - { - 'server_hostname': 'ceph-dev2', - 'version': 'ceph version 13.0.0-5083- () mimic (dev)', - 'optimized_paths': 0, 'non_optimized_paths': 1}], - 'images': [{ - 'device_id': 'pool1/image1', - 'pool_name': 'pool1', - 'name': 'image1', - 'id': '42', - 'optimized_paths': ['ceph-dev1'], - 'non_optimized_paths': ['ceph-dev2'], - 'optimized_daemon': 'ceph-dev1', - 'optimized_since': 1152121348, - 'stats': { - 'rd': 43.0, - 'wr': 44.0, - 'rd_bytes': 45.0, - 'wr_bytes': 46.0}, - 'stats_history': { - 'rd': [[1, 43]], - 'wr': [[1, 44]], - 'rd_bytes': [[1, 45]], - 'wr_bytes': [[1, 46]]} - }] - }) -- 2.39.5