From 69cfa3a4f04147d270f47d71e06c930b5c65a367 Mon Sep 17 00:00:00 2001 From: Varsha Date: Tue, 25 Jun 2019 16:20:06 +0530 Subject: [PATCH] Add Simple Storage resource support Simple Storage resource is a storage model used to represent the properties of a storage controller and its directly attached devices. As of this patch, the Simple Storage emulation is only supported for the libvirt driver for storage devices that are configured as a Volume via libvirt and attached to a VM/domain. Story: #2005948 Task: #34313 Change-Id: If10bd04d517600c3a69bb43fe0ea50f2efd62422 --- doc/source/user/dynamic-emulator.rst | 56 +++++++ ...ple-storage-resource-200e78d78c6aa8df.yaml | 8 + sushy_tools/emulator/main.py | 31 ++++ .../emulator/resources/systems/base.py | 7 + .../resources/systems/libvirtdriver.py | 94 ++++++++++++ .../emulator/resources/systems/novadriver.py | 3 + .../emulator/templates/simple_storage.json | 21 +++ .../templates/simple_storage_collection.json | 16 ++ .../unit/emulator/domain_simple_storage.xml | 42 +++++ .../resources/systems/test_libvirt.py | 78 ++++++++++ .../emulator/resources/systems/test_nova.py | 5 + sushy_tools/tests/unit/emulator/test_main.py | 144 ++++++++++++++++++ 12 files changed, 505 insertions(+) create mode 100644 releasenotes/notes/add-simple-storage-resource-200e78d78c6aa8df.yaml create mode 100644 sushy_tools/emulator/templates/simple_storage.json create mode 100644 sushy_tools/emulator/templates/simple_storage_collection.json create mode 100644 sushy_tools/tests/unit/emulator/domain_simple_storage.xml diff --git a/doc/source/user/dynamic-emulator.rst b/doc/source/user/dynamic-emulator.rst index b40946cb..d3f989de 100644 --- a/doc/source/user/dynamic-emulator.rst +++ b/doc/source/user/dynamic-emulator.rst @@ -85,6 +85,62 @@ You should be able to flip its power state via the Redfish call: You can have as many domains as you need. The domains can be concurrently managed over Redfish and some other tool like *Virtual BMC*. + +Simple Storage resource +~~~~~~~~~~~~~~~~~~~~~~~ + +For emulating the *Simple Storage* resource, some additional preparation is +required on the host side. + +First, you need to create, build and start a libvirt storage pool using virsh: + +.. code-block:: bash + + virsh pool-define-as testPool dir - - - - "/testPool" + virsh pool-build testPool + virsh pool-start testPool + virsh pool-autostart testPool + +Next, create a storage volume in the above created storage pool: + +.. code-block:: bash + + virsh vol-create-as testPool testVol 1G + +Next, attach the created volume to the virtual machine/domain: + +.. code-block:: bash + + virsh attach-disk vbmc-node /testPool/testVol sda + +Now, query the *Simple Storage* resource collection for the `vbmc-node` domain +in a closely similar format (with 'ide' and 'scsi', here, referring to the two +Redfish Simple Storage Controllers available for this domain): + +.. code-block:: bash + + curl http://localhost:8000/redfish/v1/vbmc-node/SimpleStorage + { + "@odata.type": "#SimpleStorageCollection.SimpleStorageCollection", + "Name": "Simple Storage Collection", + "Members@odata.count": 2, + "Members": [ + + { + "@odata.id": "/redfish/v1/Systems/vbmc-node/SimpleStorage/ide" + }, + + { + "@odata.id": "/redfish/v1/Systems/vbmc-node/SimpleStorage/scsi" + } + + ], + "Oem": {}, + "@odata.context": "/redfish/v1/$metadata#SimpleStorageCollection.SimpleStorageCollection", + "@odata.id": "/redfish/v1/Systems/vbmc-node/SimpleStorage" + } + + UEFI boot ~~~~~~~~~ diff --git a/releasenotes/notes/add-simple-storage-resource-200e78d78c6aa8df.yaml b/releasenotes/notes/add-simple-storage-resource-200e78d78c6aa8df.yaml new file mode 100644 index 00000000..bc4177f6 --- /dev/null +++ b/releasenotes/notes/add-simple-storage-resource-200e78d78c6aa8df.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Adds emulation support for Simple Storage resource to libvirt + virtualization backend of the dynamic Redfish emulator. The emulation + functionality assumes that the storage devices attached to a VM are + configured as a libvirt Volume via a storage pool. Devices not configured + as a Volume will not be considered for emulation purposes. diff --git a/sushy_tools/emulator/main.py b/sushy_tools/emulator/main.py index 58a5b1c1..b45bba99 100755 --- a/sushy_tools/emulator/main.py +++ b/sushy_tools/emulator/main.py @@ -626,6 +626,37 @@ def system_reset_bios(identity): return '', 204 +@app.route('/redfish/v1/Systems//SimpleStorage', + methods=['GET']) +@ensure_instance_access +@returns_json +def simple_storage_collection(identity): + with Resources() as resources: + simple_storage_controllers = ( + resources.systems.get_simple_storage_collection(identity)) + + return flask.render_template( + 'simple_storage_collection.json', identity=identity, + simple_storage_controllers=simple_storage_controllers) + + +@app.route('/redfish/v1/Systems//SimpleStorage/', + methods=['GET']) +@ensure_instance_access +@returns_json +def simple_storage(identity, simple_storage_id): + with Resources() as resources: + simple_storage_controllers = ( + resources.systems.get_simple_storage_collection(identity)) + try: + storage_controller = simple_storage_controllers[simple_storage_id] + except KeyError: + app.logger.debug('"%s" Simple Storage resource was not found') + return 'Not found', 404 + return flask.render_template('simple_storage.json', identity=identity, + simple_storage=storage_controller) + + def parse_args(): parser = argparse.ArgumentParser('sushy-emulator') parser.add_argument('--config', diff --git a/sushy_tools/emulator/resources/systems/base.py b/sushy_tools/emulator/resources/systems/base.py index c0feb554..56e2e858 100644 --- a/sushy_tools/emulator/resources/systems/base.py +++ b/sushy_tools/emulator/resources/systems/base.py @@ -192,3 +192,10 @@ class AbstractSystemsDriver(DriverBase): :raises: `error.FishyError` if boot device can't be set """ + + @abc.abstractmethod + def get_simple_storage_collection(self, identity): + """Get a dict of Simple Storage Controllers and their devices + + :returns: dict of Simple Storage Controllers and their atributes + """ diff --git a/sushy_tools/emulator/resources/systems/libvirtdriver.py b/sushy_tools/emulator/resources/systems/libvirtdriver.py index b6c79216..ce8480af 100644 --- a/sushy_tools/emulator/resources/systems/libvirtdriver.py +++ b/sushy_tools/emulator/resources/systems/libvirtdriver.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from collections import defaultdict from collections import namedtuple import logging import os @@ -839,3 +840,96 @@ class LibvirtDriver(AbstractSystemsDriver): '%(error)s' % {'uri': self._uri, 'error': e}) raise error.FishyError(msg) + + def _find_device_by_path(self, vol_path): + """Get device attributes using path + + :param vol_path: path for the libvirt volume + :returns: a dict (or None) of the corresponding device attributes + """ + with libvirt_open(self._uri, readonly=True) as conn: + try: + vol = conn.storageVolLookupByPath(vol_path) + except libvirt.libvirtError as e: + msg = ('Could not find storage volume by path ' + '"%(path)s" at libvirt URI "%(uri)s": ' + '%(err)s' % + {'path': vol_path, 'uri': self._uri, + 'err': e}) + logger.debug(msg) + return + disk_device = { + 'Name': vol.name(), + 'CapacityBytes': vol.info()[1] + } + return disk_device + + def _find_device_from_pool(self, pool_name, vol_name): + """Get device attributes from pool + + :param pool_name: libvirt pool name + :param vol_name: libvirt volume name + :returns: a dict (or None) of the corresponding device attributes + """ + with libvirt_open(self._uri, readonly=True) as conn: + try: + pool = conn.storagePoolLookupByName(pool_name) + except libvirt.libvirtError as e: + msg = ('Error finding Storage Pool by name "%(name)s" at' + 'libvirt URI "%(uri)s": %(err)s' % + {'name': pool_name, 'uri': self._uri, 'err': e}) + logger.debug(msg) + return + + try: + vol = pool.storageVolLookupByName(vol_name) + except libvirt.libvirtError as e: + msg = ('Error finding Storage Volume by name "%(name)s" ' + 'in Pool '"%(pName)s"' at libvirt URI "%(uri)s"' + ': %(err)s' % + {'name': vol_name, 'pName': pool_name, + 'uri': self._uri, 'err': e}) + logger.debug(msg) + return + disk_device = { + 'Name': vol.name(), + 'CapacityBytes': vol.info()[1] + } + return disk_device + + def get_simple_storage_collection(self, identity): + """Get a dict of simple storage controllers and their devices + + Only those storage devices that are configured as a libvirt volume + via a pool and attached to the domain will reflect as a device. + Others are skipped. + + :param identity: libvirt domain or ID + :returns: dict of simple storage controller dict with their attributes + """ + domain = self._get_domain(identity, readonly=True) + tree = ET.fromstring(domain.XMLDesc()) + simple_storage = defaultdict(lambda: defaultdict(DeviceList=list())) + + for disk_element in tree.findall(".//disk/target[@bus]/.."): + source_element = disk_element.find('source') + if source_element is not None: + disk_type = disk_element.attrib['type'] + ctl_type = disk_element.find('target').attrib['bus'] + disk_device = None + if disk_type in ('file', 'block'): + if disk_type == 'file': + vol_path = source_element.attrib['file'] + else: + vol_path = source_element.attrib['dev'] + disk_device = self._find_device_by_path(vol_path) + elif disk_type == 'volume': + pool_name = source_element.attrib['pool'] + vol_name = source_element.attrib['volume'] + disk_device = self._find_device_from_pool(pool_name, + vol_name) + if disk_device is not None: + simple_storage[ctl_type]['Id'] = ctl_type + simple_storage[ctl_type]['Name'] = ctl_type + simple_storage[ctl_type]['DeviceList'].append(disk_device) + return simple_storage diff --git a/sushy_tools/emulator/resources/systems/novadriver.py b/sushy_tools/emulator/resources/systems/novadriver.py index ca37e93a..9094820c 100644 --- a/sushy_tools/emulator/resources/systems/novadriver.py +++ b/sushy_tools/emulator/resources/systems/novadriver.py @@ -371,3 +371,6 @@ class OpenStackDriver(AbstractSystemsDriver): :raises: `error.FishyError` if boot device can't be set """ raise error.NotSupportedError('Not implemented') + + def get_simple_storage_collection(self, identity): + raise error.NotSupportedError('Not implemented') diff --git a/sushy_tools/emulator/templates/simple_storage.json b/sushy_tools/emulator/templates/simple_storage.json new file mode 100644 index 00000000..baf74303 --- /dev/null +++ b/sushy_tools/emulator/templates/simple_storage.json @@ -0,0 +1,21 @@ +{ + "@odata.type": "#SimpleStorage.v1_2_0.SimpleStorage", + "Id": {{ simple_storage['Id']|string|tojson }}, + "Name": {{ "%s Controller"|format(simple_storage['Name'])|tojson }}, + "Devices": [ + {% for device in simple_storage['DeviceList'] %} + { + "@odata.type": "#SimpleStorage.v1_1_0.Device", + "Name": {{ device['Name']|string|tojson }}, + "CapacityBytes": {{ device['CapacityBytes'] }}, + "Status": { + "@odata.type": "#Resource.Status", + "State": "Enabled", + "Health": "OK" + } + }{% if not loop.last %},{% endif %} + {% endfor %} + ], + "@odata.context": "/redfish/v1/$metadata#SimpleStorage.SimpleStorage", + "@odata.id": {{ "/redfish/v1/Systems/%s/SimpleStorage/%s"|format(identity, simple_storage['Id'])|tojson }} +} \ No newline at end of file diff --git a/sushy_tools/emulator/templates/simple_storage_collection.json b/sushy_tools/emulator/templates/simple_storage_collection.json new file mode 100644 index 00000000..30ef7042 --- /dev/null +++ b/sushy_tools/emulator/templates/simple_storage_collection.json @@ -0,0 +1,16 @@ +{ + "@odata.type": "#SimpleStorageCollection.SimpleStorageCollection", + "Name": "Simple Storage Collection", + "Members@odata.count": {{ simple_storage_controllers|length }}, + "Members": [ + {% for simple_storage in simple_storage_controllers %} + { + "@odata.id": {{ "/redfish/v1/Systems/%s/SimpleStorage/%s"|format(identity, simple_storage_controllers[simple_storage]['Id'])|tojson }} + }{% if not loop.last %},{% endif %} + {% endfor %} + ], + "Oem": {}, + "@odata.context": "/redfish/v1/$metadata#SimpleStorageCollection.SimpleStorageCollection", + "@odata.id": {{ "/redfish/v1/Systems/%s/SimpleStorage"|format(identity)|tojson }} +} + diff --git a/sushy_tools/tests/unit/emulator/domain_simple_storage.xml b/sushy_tools/tests/unit/emulator/domain_simple_storage.xml new file mode 100644 index 00000000..4a22298f --- /dev/null +++ b/sushy_tools/tests/unit/emulator/domain_simple_storage.xml @@ -0,0 +1,42 @@ + + QEmu-fedora-i686 + c7a5fdbd-cdaf-9455-926a-d65c16db1809 + 219200 + 219200 + 2 + + hvm + + + + + /usr/bin/qemu-system-x86_64 + + + + +
+ + + + + +
+ + + + + +
+ + + + + + + + + + + + diff --git a/sushy_tools/tests/unit/emulator/resources/systems/test_libvirt.py b/sushy_tools/tests/unit/emulator/resources/systems/test_libvirt.py index 0fc28c5b..24ff7622 100644 --- a/sushy_tools/tests/unit/emulator/resources/systems/test_libvirt.py +++ b/sushy_tools/tests/unit/emulator/resources/systems/test_libvirt.py @@ -567,3 +567,81 @@ class LibvirtDriverTestCase(base.BaseTestCase): nics = self.test_driver.get_nics(self.uuid) self.assertEqual([], nics) + + @mock.patch('libvirt.openReadOnly', autospec=True) + def test_get_simple_storage_collection(self, libvirt_mock): + with open('sushy_tools/tests/unit/emulator/' + 'domain_simple_storage.xml', 'r') as f: + data = f.read() + + conn_mock = libvirt_mock.return_value + dom_mock = conn_mock.lookupByUUID.return_value + dom_mock.XMLDesc.return_value = data + vol_mock = conn_mock.storageVolLookupByPath.return_value + vol_mock.name.side_effect = ['testVM1.img', 'testVol1.img', 'sdb1'] + vol_mock.info.side_effect = [['testVM1.img', 100000], + ['testVol1.img', 200000], + ['sdb1', 150000]] + pool_mock = conn_mock.storagePoolLookupByName.return_value + pvol_mock = pool_mock.storageVolLookupByName.return_value + pvol_mock.name.return_value = 'blk-pool0-vol0' + pvol_mock.info.return_value = ['volType', 300000] + + simple_storage_response = (self.test_driver + .get_simple_storage_collection(self.uuid)) + + simple_storage_expected = { + 'virtio': { + 'Id': 'virtio', + 'Name': 'virtio', + 'DeviceList': [ + { + 'Name': 'testVM1.img', + 'CapacityBytes': 100000 + }, + { + 'Name': 'sdb1', + 'CapacityBytes': 150000 + } + ] + }, + 'ide': { + 'Id': 'ide', + 'Name': 'ide', + 'DeviceList': [ + { + 'Name': 'testVol1.img', + 'CapacityBytes': 200000 + }, + { + 'Name': 'blk-pool0-vol0', + 'CapacityBytes': 300000 + } + ] + } + } + + self.assertEqual(simple_storage_response, simple_storage_expected) + + @mock.patch('libvirt.openReadOnly', autospec=True) + def test_get_simple_storage_collection_empty(self, libvirt_mock): + with open('sushy_tools/tests/unit/emulator/domain.xml') as f: + domain_xml = f.read() + + conn_mock = libvirt_mock.return_value + dom_mock = conn_mock.lookupByUUID.return_value + dom_mock.XMLDesc.return_value = domain_xml + vol_mock = conn_mock.storageVolLookupByPath.return_value + vol_mock.name.side_effect = ['testVM1.img', 'testVol1.img', 'sdb1'] + vol_mock.info.side_effect = [['testType1', 100000], + ['testType2', 200000], + ['testType1', 150000]] + pool_mock = conn_mock.storagePoolLookupByName.return_value + pvol_mock = pool_mock.storageVolLookupByName.return_value + pvol_mock.name.return_value = 'blk-pool0-vol0' + pvol_mock.info.return_value = ['volType', 300000] + + simple_storage_response = (self.test_driver + .get_simple_storage_collection(self.uuid)) + + self.assertEqual({}, simple_storage_response) diff --git a/sushy_tools/tests/unit/emulator/resources/systems/test_nova.py b/sushy_tools/tests/unit/emulator/resources/systems/test_nova.py index cedc1315..7d9b11dd 100644 --- a/sushy_tools/tests/unit/emulator/resources/systems/test_nova.py +++ b/sushy_tools/tests/unit/emulator/resources/systems/test_nova.py @@ -256,3 +256,8 @@ class NovaDriverTestCase(base.BaseTestCase): nics = self.test_driver.get_nics(self.uuid) self.assertEqual([{'id': 'fa:16:3e:22:18:31', 'mac': 'fa:16:3e:22:18:31'}], nics) + + def test_get_simple_storage_collection(self): + self.assertRaises( + error.FishyError, + self.test_driver.get_simple_storage_collection, self.uuid) diff --git a/sushy_tools/tests/unit/emulator/test_main.py b/sushy_tools/tests/unit/emulator/test_main.py index eae639b6..f16303d7 100644 --- a/sushy_tools/tests/unit/emulator/test_main.py +++ b/sushy_tools/tests/unit/emulator/test_main.py @@ -574,3 +574,147 @@ class EmulatorTestCase(base.BaseTestCase): self.assertEqual(204, response.status_code) vmedia_mock.eject_image.called_once_with('CD') + + def test_simple_storage_collection(self, resources_mock): + resources_mock = resources_mock.return_value.__enter__.return_value + systems_mock = resources_mock.systems + systems_mock.get_simple_storage_collection.return_value = { + 'virtio': { + 'Id': 'virtio', + 'Name': 'virtio', + 'DeviceList': [ + { + 'Name': 'testVM1.img', + 'CapacityBytes': 100000 + }, + { + 'Name': 'sdb1', + 'CapacityBytes': 150000 + } + ] + }, + 'ide': { + 'Id': 'ide', + 'Name': 'ide', + 'DeviceList': [ + { + 'Name': 'testVol1.img', + 'CapacityBytes': 200000 + }, + { + 'Name': 'blk-pool0-vol0', + 'CapacityBytes': 300000 + } + ] + } + } + response = self.app.get('redfish/v1/Systems/' + self.uuid + + '/SimpleStorage') + + self.assertEqual(200, response.status_code) + self.assertEqual('Simple Storage Collection', + response.json['Name']) + self.assertEqual(2, response.json['Members@odata.count']) + self.assertEqual({'/redfish/v1/Systems/' + self.uuid + + '/SimpleStorage/virtio', + '/redfish/v1/Systems/' + self.uuid + + '/SimpleStorage/ide'}, + {m['@odata.id'] for m in response.json['Members']}) + + def test_simple_storage_collection_empty(self, resources_mock): + resources_mock = resources_mock.return_value.__enter__.return_value + systems_mock = resources_mock.systems + systems_mock.get_simple_storage_collection.return_value = [] + response = self.app.get('redfish/v1/Systems/' + self.uuid + + '/SimpleStorage') + + self.assertEqual(200, response.status_code) + self.assertEqual('Simple Storage Collection', + response.json['Name']) + self.assertEqual(0, response.json['Members@odata.count']) + self.assertEqual([], response.json['Members']) + + def test_simple_storage(self, resources_mock): + resources_mock = resources_mock.return_value.__enter__.return_value + systems_mock = resources_mock.systems + systems_mock.get_simple_storage_collection.return_value = { + 'virtio': { + 'Id': 'virtio', + 'Name': 'virtio', + 'DeviceList': [ + { + 'Name': 'testVM1.img', + 'CapacityBytes': 100000 + }, + { + 'Name': 'sdb1', + 'CapacityBytes': 150000 + } + ] + }, + 'ide': { + 'Id': 'ide', + 'Name': 'ide', + 'DeviceList': [ + { + 'Name': 'testVol1.img', + 'CapacityBytes': 200000 + }, + { + 'Name': 'blk-pool0-vol0', + 'CapacityBytes': 300000 + } + ] + } + } + response = self.app.get('/redfish/v1/Systems/' + self.uuid + + '/SimpleStorage/virtio') + + self.assertEqual(200, response.status_code) + self.assertEqual('virtio', response.json['Id']) + self.assertEqual('virtio Controller', response.json['Name']) + self.assertEqual('testVM1.img', response.json['Devices'][0]['Name']) + self.assertEqual(100000, response.json['Devices'][0]['CapacityBytes']) + self.assertEqual('sdb1', response.json['Devices'][1]['Name']) + self.assertEqual(150000, response.json['Devices'][1]['CapacityBytes']) + self.assertEqual('/redfish/v1/Systems/' + self.uuid + + '/SimpleStorage/virtio', + response.json['@odata.id']) + + def test_simple_storage_not_found(self, resources_mock): + resources_mock = resources_mock.return_value.__enter__.return_value + systems_mock = resources_mock.systems + systems_mock.get_simple_storage_collection.return_value = { + 'virtio': { + 'Id': 'virtio', + 'Name': 'virtio', + 'DeviceList': [ + { + 'Name': 'testVM1.img', + 'CapacityBytes': 100000 + }, + { + 'Name': 'sdb1', + 'CapacityBytes': 150000 + } + ] + }, + 'ide': { + 'Id': 'ide', + 'Name': 'ide', + 'DeviceList': [ + { + 'Name': 'testVol1.img', + 'CapacityBytes': 200000 + }, + { + 'Name': 'blk-pool0-vol0', + 'CapacityBytes': 300000 + } + ] + } + } + response = self.app.get('/redfish/v1/Systems/' + self.uuid + + '/SimpleStorage/scsi') + + self.assertEqual(404, response.status_code)