From f272429e83aebb1e27cba1db3da6aeaf94506aa5 Mon Sep 17 00:00:00 2001 From: Ricardo Marques Date: Fri, 22 Mar 2019 18:53:29 +0000 Subject: [PATCH] mgr/dashboard: Validate iSCSI images features Will also filter the list of available images. Fixes: https://tracker.ceph.com/issues/38074 Signed-off-by: Ricardo Marques --- src/pybind/mgr/dashboard/controllers/iscsi.py | 23 +++++- .../iscsi-target-form.component.spec.ts | 52 +++++++------ .../iscsi-target-form.component.ts | 76 +++++++++++++++---- src/pybind/mgr/dashboard/tests/test_iscsi.py | 64 +++++++++------- 4 files changed, 149 insertions(+), 66 deletions(-) diff --git a/src/pybind/mgr/dashboard/controllers/iscsi.py b/src/pybind/mgr/dashboard/controllers/iscsi.py index 5342a4463b592..e684bef61aaee 100644 --- a/src/pybind/mgr/dashboard/controllers/iscsi.py +++ b/src/pybind/mgr/dashboard/controllers/iscsi.py @@ -350,14 +350,31 @@ class IscsiTarget(RESTController): for disk in disks: pool = disk['pool'] image = disk['image'] - IscsiTarget._validate_image_exists(pool, image) + backstore = disk['backstore'] + required_rbd_features = settings['required_rbd_features'][backstore] + supported_rbd_features = settings['supported_rbd_features'][backstore] + IscsiTarget._validate_image(pool, image, backstore, required_rbd_features, + supported_rbd_features) @staticmethod - def _validate_image_exists(pool, image): + def _validate_image(pool, image, backstore, required_rbd_features, supported_rbd_features): try: ioctx = mgr.rados.open_ioctx(pool) try: - rbd.Image(ioctx, image) + with rbd.Image(ioctx, image) as img: + if img.features() & required_rbd_features != required_rbd_features: + raise DashboardException(msg='Image {} cannot be exported using {} ' + 'backstore because required features are ' + 'missing'.format(image, backstore), + code='image_missing_required_features', + component='iscsi') + if img.features() & supported_rbd_features != img.features(): + raise DashboardException(msg='Image {} cannot be exported using {} ' + 'backstore because it contains unsupported ' + 'features'.format(image, backstore), + code='image_contains_unsupported_features', + component='iscsi') + except rbd.ImageNotFound: raise DashboardException(msg='Image {} does not exist'.format(image), code='image_does_not_exist', diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.spec.ts index 5ee56d2b4f8e3..29fc65aa22ea2 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.spec.ts @@ -33,6 +33,14 @@ describe('IscsiTargetFormComponent', () => { dataout_timeout: 20, immediate_data: 'Yes' }, + required_rbd_features: { + 'backstore:1': 0, + 'backstore:2': 0 + }, + supported_rbd_features: { + 'backstore:1': 61, + 'backstore:2': 61 + }, backstores: ['backstore:1', 'backstore:2'], default_backstore: 'backstore:1' }; @@ -41,7 +49,7 @@ describe('IscsiTargetFormComponent', () => { { target_iqn: 'iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw', portals: [{ host: 'node1', ip: '192.168.100.201' }], - disks: [{ pool: 'rbd', image: 'disk_1', controls: {} }], + disks: [{ pool: 'rbd', image: 'disk_1', controls: {}, backstore: 'backstore:1' }], clients: [ { client_iqn: 'iqn.1994-05.com.redhat:rh7-client', @@ -155,7 +163,7 @@ describe('IscsiTargetFormComponent', () => { }); it('should only show images not used in other targets', () => { - expect(component.imagesAll).toEqual(['rbd/disk_2']); + expect(component.imagesAll).toEqual([RBD_LIST[1]['value'][1]]); expect(component.imagesSelections).toEqual([ { description: '', name: 'rbd/disk_2', selected: false } ]); @@ -183,9 +191,9 @@ describe('IscsiTargetFormComponent', () => { it('should prepare data when selecting an image', () => { expect(component.imagesSettings).toEqual({}); - component.onImageSelection({ option: { name: 'rbd/disk_1', selected: true } }); + component.onImageSelection({ option: { name: 'rbd/disk_2', selected: true } }); expect(component.imagesSettings).toEqual({ - 'rbd/disk_1': { + 'rbd/disk_2': { backstore: 'backstore:1', 'backstore:1': {} } @@ -193,24 +201,24 @@ describe('IscsiTargetFormComponent', () => { }); it('should clean data when removing an image', () => { - component.onImageSelection({ option: { name: 'rbd/disk_1', selected: true } }); + component.onImageSelection({ option: { name: 'rbd/disk_2', selected: true } }); component.addGroup(); component.groups.controls[0].patchValue({ group_id: 'foo', - disks: ['rbd/disk_1'] + disks: ['rbd/disk_2'] }); expect(component.groups.controls[0].value).toEqual({ - disks: ['rbd/disk_1'], + disks: ['rbd/disk_2'], group_id: 'foo', members: [] }); - component.onImageSelection({ option: { name: 'rbd/disk_1', selected: false } }); + component.onImageSelection({ option: { name: 'rbd/disk_2', selected: false } }); expect(component.groups.controls[0].value).toEqual({ disks: [], group_id: 'foo', members: [] }); expect(component.imagesSettings).toEqual({ - 'rbd/disk_1': { + 'rbd/disk_2': { backstore: 'backstore:1', 'backstore:1': {} } @@ -219,9 +227,9 @@ describe('IscsiTargetFormComponent', () => { describe('should test initiators', () => { beforeEach(() => { - component.targetForm.patchValue({ disks: ['rbd/disk_1'], acl_enabled: true }); + component.targetForm.patchValue({ disks: ['rbd/disk_2'], acl_enabled: true }); component.addGroup().patchValue({ name: 'group_1' }); - component.onImageSelection({ option: { name: 'rbd/disk_1', selected: true } }); + component.onImageSelection({ option: { name: 'rbd/disk_2', selected: true } }); component.addInitiator(); component.initiators.controls[0].patchValue({ @@ -239,7 +247,7 @@ describe('IscsiTargetFormComponent', () => { luns: [] }); expect(component.imagesInitiatorSelections).toEqual([ - [{ description: '', name: 'rbd/disk_1', selected: false }] + [{ description: '', name: 'rbd/disk_2', selected: false }] ]); expect(component.groupMembersSelections).toEqual([ [{ description: '', name: 'iqn.initiator', selected: false }] @@ -286,13 +294,13 @@ describe('IscsiTargetFormComponent', () => { it('should remove images in the initiator when added in a group', () => { component.initiators.controls[0].patchValue({ - luns: ['rbd/disk_1'] + luns: ['rbd/disk_2'] }); expect(component.initiators.controls[0].value).toEqual({ auth: { mutual_password: '', mutual_user: '', password: '', user: '' }, cdIsInGroup: false, client_iqn: 'iqn.initiator', - luns: ['rbd/disk_1'] + luns: ['rbd/disk_2'] }); component.addGroup(); @@ -318,8 +326,8 @@ describe('IscsiTargetFormComponent', () => { describe('should submit request', () => { beforeEach(() => { - component.targetForm.patchValue({ disks: ['rbd/disk_1'], acl_enabled: true }); - component.onImageSelection({ option: { name: 'rbd/disk_1', selected: true } }); + component.targetForm.patchValue({ disks: ['rbd/disk_2'], acl_enabled: true }); + component.onImageSelection({ option: { name: 'rbd/disk_2', selected: true } }); component.portals.setValue(['node1:192.168.100.201', 'node2:192.168.100.202']); component.addInitiator().patchValue({ client_iqn: 'iqn.initiator' @@ -327,7 +335,7 @@ describe('IscsiTargetFormComponent', () => { component.addGroup().patchValue({ group_id: 'foo', members: ['iqn.initiator'], - disks: ['rbd/disk_1'] + disks: ['rbd/disk_2'] }); }); @@ -349,9 +357,9 @@ describe('IscsiTargetFormComponent', () => { luns: [] } ], - disks: [{ backstore: 'backstore:1', controls: {}, image: 'disk_1', pool: 'rbd' }], + disks: [{ backstore: 'backstore:1', controls: {}, image: 'disk_2', pool: 'rbd' }], groups: [ - { disks: [{ image: 'disk_1', pool: 'rbd' }], group_id: 'foo', members: ['iqn.initiator'] } + { disks: [{ image: 'disk_2', pool: 'rbd' }], group_id: 'foo', members: ['iqn.initiator'] } ], new_target_iqn: component.targetForm.value.target_iqn, portals: [ @@ -378,10 +386,10 @@ describe('IscsiTargetFormComponent', () => { luns: [] } ], - disks: [{ backstore: 'backstore:1', controls: {}, image: 'disk_1', pool: 'rbd' }], + disks: [{ backstore: 'backstore:1', controls: {}, image: 'disk_2', pool: 'rbd' }], groups: [ { - disks: [{ image: 'disk_1', pool: 'rbd' }], + disks: [{ image: 'disk_2', pool: 'rbd' }], group_id: 'foo', members: ['iqn.initiator'] } @@ -404,7 +412,7 @@ describe('IscsiTargetFormComponent', () => { expect(req.request.method).toBe('POST'); expect(req.request.body).toEqual({ clients: [], - disks: [{ backstore: 'backstore:1', controls: {}, image: 'disk_1', pool: 'rbd' }], + disks: [{ backstore: 'backstore:1', controls: {}, image: 'disk_2', pool: 'rbd' }], groups: [], acl_enabled: false, portals: [ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts index 99127652b7c01..bd68a3399775b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi-target-form/iscsi-target-form.component.ts @@ -31,6 +31,8 @@ export class IscsiTargetFormComponent implements OnInit { disk_default_controls: any; backstores: string[]; default_backstore: string; + supported_rbd_features: any; + required_rbd_features: any; isEdit = false; target_iqn: string; @@ -109,14 +111,34 @@ export class IscsiTargetFormComponent implements OnInit { .map((image) => `${image.pool}/${image.image}`) .value(); + // iscsiService.settings() + this.minimum_gateways = data[3].config.minimum_gateways; + this.target_default_controls = data[3].target_default_controls; + this.disk_default_controls = data[3].disk_default_controls; + this.backstores = data[3].backstores; + this.default_backstore = data[3].default_backstore; + this.supported_rbd_features = data[3].supported_rbd_features; + this.required_rbd_features = data[3].required_rbd_features; + // rbdService.list() this.imagesAll = _(data[1]) .flatMap((pool) => pool.value) - .map((image) => `${image.pool_name}/${image.name}`) - .filter((image) => usedImages.indexOf(image) === -1) + .filter((image) => { + const imageId = `${image.pool_name}/${image.name}`; + if (usedImages.indexOf(imageId) !== -1) { + return false; + } + const validBackstores = this.getValidBackstores(image); + if (validBackstores.length === 0) { + return false; + } + return true; + }) .value(); - this.imagesSelections = this.imagesAll.map((image) => new SelectOption(false, image, '')); + this.imagesSelections = this.imagesAll.map( + (image) => new SelectOption(false, `${image.pool_name}/${image.name}`, '') + ); // iscsiService.portals() const portals: SelectOption[] = []; @@ -127,13 +149,6 @@ export class IscsiTargetFormComponent implements OnInit { }); this.portalsSelections = [...portals]; - // iscsiService.settings() - this.minimum_gateways = data[3].config.minimum_gateways; - this.target_default_controls = data[3].target_default_controls; - this.disk_default_controls = data[3].disk_default_controls; - this.backstores = data[3].backstores; - this.default_backstore = data[3].default_backstore; - this.createForm(); // iscsiService.getTarget() @@ -273,15 +288,31 @@ export class IscsiTargetFormComponent implements OnInit { }); } + getDefaultBackstore(imageId) { + let result = this.default_backstore; + const image = this.getImageById(imageId); + if (!this.validFeatures(image, this.default_backstore)) { + this.backstores.forEach((backstore) => { + if (backstore !== this.default_backstore) { + if (this.validFeatures(image, backstore)) { + result = backstore; + } + } + }); + } + return result; + } + onImageSelection($event) { const option = $event.option; if (option.selected) { if (!this.imagesSettings[option.name]) { + const defaultBackstore = this.getDefaultBackstore(option.name); this.imagesSettings[option.name] = { - backstore: this.default_backstore + backstore: defaultBackstore }; - this.imagesSettings[option.name][this.default_backstore] = {}; + this.imagesSettings[option.name][defaultBackstore] = {}; } _.forEach(this.imagesInitiatorSelections, (selections, i) => { @@ -628,11 +659,30 @@ export class IscsiTargetFormComponent implements OnInit { imagesSettings: this.imagesSettings, image: image, disk_default_controls: this.disk_default_controls, - backstores: this.backstores + backstores: this.getValidBackstores(this.getImageById(image)) }; this.modalRef = this.modalService.show(IscsiTargetImageSettingsModalComponent, { initialState }); } + + validFeatures(image, backstore) { + const imageFeatures = image.features; + const requiredFeatures = this.required_rbd_features[backstore]; + const supportedFeatures = this.supported_rbd_features[backstore]; + // tslint:disable-next-line:no-bitwise + const validRequiredFeatures = (imageFeatures & requiredFeatures) === requiredFeatures; + // tslint:disable-next-line:no-bitwise + const validSupportedFeatures = (imageFeatures & supportedFeatures) === imageFeatures; + return validRequiredFeatures && validSupportedFeatures; + } + + getImageById(imageId) { + return this.imagesAll.find((image) => imageId === `${image.pool_name}/${image.name}`); + } + + getValidBackstores(image) { + return this.backstores.filter((backstore) => this.validFeatures(image, backstore)); + } } diff --git a/src/pybind/mgr/dashboard/tests/test_iscsi.py b/src/pybind/mgr/dashboard/tests/test_iscsi.py index c8d99eea280ab..db88f32f0914c 100644 --- a/src/pybind/mgr/dashboard/tests/test_iscsi.py +++ b/src/pybind/mgr/dashboard/tests/test_iscsi.py @@ -98,8 +98,8 @@ class IscsiTest(ControllerTestCase, CLICommandTestMixin): self.assertStatus(200) self.assertJsonBody([]) - @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists') - def test_list(self, _validate_image_exists_mock): + @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image') + def test_list(self, _validate_image_mock): target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw1" request = copy.deepcopy(iscsi_target_request) request['target_iqn'] = target_iqn @@ -111,8 +111,8 @@ class IscsiTest(ControllerTestCase, CLICommandTestMixin): response['target_iqn'] = target_iqn self.assertJsonBody([response]) - @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists') - def test_create(self, _validate_image_exists_mock): + @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image') + def test_create(self, _validate_image_mock): target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw2" request = copy.deepcopy(iscsi_target_request) request['target_iqn'] = target_iqn @@ -124,8 +124,8 @@ class IscsiTest(ControllerTestCase, CLICommandTestMixin): response['target_iqn'] = target_iqn self.assertJsonBody(response) - @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists') - def test_delete(self, _validate_image_exists_mock): + @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image') + def test_delete(self, _validate_image_mock): target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw3" request = copy.deepcopy(iscsi_target_request) request['target_iqn'] = target_iqn @@ -137,8 +137,8 @@ class IscsiTest(ControllerTestCase, CLICommandTestMixin): self.assertStatus(200) self.assertJsonBody([]) - @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists') - def test_add_client(self, _validate_image_exists_mock): + @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image') + def test_add_client(self, _validate_image_mock): target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw4" create_request = copy.deepcopy(iscsi_target_request) create_request['target_iqn'] = target_iqn @@ -168,8 +168,8 @@ class IscsiTest(ControllerTestCase, CLICommandTestMixin): }) self._update_iscsi_target(create_request, update_request, response) - @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists') - def test_change_client_password(self, _validate_image_exists_mock): + @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image') + def test_change_client_password(self, _validate_image_mock): target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw5" create_request = copy.deepcopy(iscsi_target_request) create_request['target_iqn'] = target_iqn @@ -181,8 +181,8 @@ class IscsiTest(ControllerTestCase, CLICommandTestMixin): response['clients'][0]['auth']['password'] = 'mynewiscsipassword' self._update_iscsi_target(create_request, update_request, response) - @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists') - def test_rename_client(self, _validate_image_exists_mock): + @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image') + def test_rename_client(self, _validate_image_mock): target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw6" create_request = copy.deepcopy(iscsi_target_request) create_request['target_iqn'] = target_iqn @@ -194,8 +194,8 @@ class IscsiTest(ControllerTestCase, CLICommandTestMixin): response['clients'][0]['client_iqn'] = 'iqn.1994-05.com.redhat:rh7-client0' self._update_iscsi_target(create_request, update_request, response) - @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists') - def test_add_disk(self, _validate_image_exists_mock): + @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image') + def test_add_disk(self, _validate_image_mock): target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw7" create_request = copy.deepcopy(iscsi_target_request) create_request['target_iqn'] = target_iqn @@ -221,8 +221,8 @@ class IscsiTest(ControllerTestCase, CLICommandTestMixin): response['clients'][0]['luns'].append({"image": "lun3", "pool": "rbd"}) self._update_iscsi_target(create_request, update_request, response) - @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists') - def test_change_disk_image(self, _validate_image_exists_mock): + @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image') + def test_change_disk_image(self, _validate_image_mock): target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw8" create_request = copy.deepcopy(iscsi_target_request) create_request['target_iqn'] = target_iqn @@ -236,8 +236,8 @@ class IscsiTest(ControllerTestCase, CLICommandTestMixin): response['clients'][0]['luns'][0]['image'] = 'lun0' self._update_iscsi_target(create_request, update_request, response) - @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists') - def test_change_disk_controls(self, _validate_image_exists_mock): + @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image') + def test_change_disk_controls(self, _validate_image_mock): target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw9" create_request = copy.deepcopy(iscsi_target_request) create_request['target_iqn'] = target_iqn @@ -249,8 +249,8 @@ class IscsiTest(ControllerTestCase, CLICommandTestMixin): response['disks'][0]['controls'] = {"qfull_timeout": 15} self._update_iscsi_target(create_request, update_request, response) - @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists') - def test_rename_target(self, _validate_image_exists_mock): + @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image') + def test_rename_target(self, _validate_image_mock): target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw10" new_target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw11" create_request = copy.deepcopy(iscsi_target_request) @@ -261,8 +261,8 @@ class IscsiTest(ControllerTestCase, CLICommandTestMixin): response['target_iqn'] = new_target_iqn self._update_iscsi_target(create_request, update_request, response) - @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists') - def test_rename_group(self, _validate_image_exists_mock): + @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image') + def test_rename_group(self, _validate_image_mock): target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw12" create_request = copy.deepcopy(iscsi_target_request) create_request['target_iqn'] = target_iqn @@ -274,8 +274,8 @@ class IscsiTest(ControllerTestCase, CLICommandTestMixin): response['groups'][0]['group_id'] = 'mygroup0' self._update_iscsi_target(create_request, update_request, response) - @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists') - def test_add_client_to_group(self, _validate_image_exists_mock): + @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image') + def test_add_client_to_group(self, _validate_image_mock): target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw13" create_request = copy.deepcopy(iscsi_target_request) create_request['target_iqn'] = target_iqn @@ -307,8 +307,8 @@ class IscsiTest(ControllerTestCase, CLICommandTestMixin): response['groups'][0]['members'].append('iqn.1994-05.com.redhat:rh7-client3') self._update_iscsi_target(create_request, update_request, response) - @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists') - def test_remove_client_from_group(self, _validate_image_exists_mock): + @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image') + def test_remove_client_from_group(self, _validate_image_mock): target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw14" create_request = copy.deepcopy(iscsi_target_request) create_request['target_iqn'] = target_iqn @@ -320,8 +320,8 @@ class IscsiTest(ControllerTestCase, CLICommandTestMixin): response['groups'][0]['members'].remove('iqn.1994-05.com.redhat:rh7-client2') self._update_iscsi_target(create_request, update_request, response) - @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image_exists') - def test_remove_groups(self, _validate_image_exists_mock): + @mock.patch('dashboard.controllers.iscsi.IscsiTarget._validate_image') + def test_remove_groups(self, _validate_image_mock): target_iqn = "iqn.2003-01.com.redhat.iscsi-gw:iscsi-igw15" create_request = copy.deepcopy(iscsi_target_request) create_request['target_iqn'] = target_iqn @@ -481,6 +481,14 @@ class IscsiClientMock(object): "minimum_gateways": 2 }, "default_backstore": "user:rbd", + "required_rbd_features": { + "rbd": 0, + "user:rbd": 4, + }, + "supported_rbd_features": { + "rbd": 135, + "user:rbd": 61, + }, "disk_default_controls": { "user:rbd": { "hw_max_sectors": 1024, -- 2.39.5