Add Volume resource support

This change adds basic Redfish Volumes resource support to the
dynamic Redfish emulator. The Volumes are emulated as persistant
mocks backed by the libvirt volumes from the libvirt virtualization
backend.

Change-Id: If99d438c07f8a37280b1c480dcdc40510283c6d9
This commit is contained in:
Varsha 2019-08-19 03:16:12 +05:30
parent f18831163e
commit fa70fbcb18
15 changed files with 557 additions and 2 deletions

View File

@ -136,4 +136,39 @@ SUSHY_EMULATOR_DRIVES = {
"Protocol": "SAS"
}
]
}
}
# This map contains dynamically configured Redfish Volume resource backed
# by the libvirt virtualization backend of the dynamic Redfish emulator.
# The Volume objects are keyed in a composite fashion using a tuple of the
# form (System_UUID, Storage_ID) referring to the UUID of the System and ID
# of the Storage resource, respectively, to which the Volume belongs.
#
# Only the volumes specified in the map or created via a POST request are
# allowed to be emulated upon by the emulator. Volumes other than these can
# neither be listed nor deleted.
#
# The Volumes from map missing in the libvirt backend will be created
# dynamically in the pool name specified (provided the pool exists in the
# backend). If the pool name is not specified, the volume will be created
# automatically in pool named 'default'.
SUSHY_EMULATOR_VOLUMES = {
('da69abcc-dae0-4913-9a7b-d344043097c0', '1'): [
{
"libvirtPoolName": "sushyPool",
"libvirtVolName": "testVol",
"Id": "1",
"Name": "Sample Volume 1",
"VolumeType": "Mirrored",
"CapacityBytes": 23748
},
{
"libvirtPoolName": "sushyPool",
"libvirtVolName": "testVol1",
"Id": "2",
"Name": "Sample Volume 2",
"VolumeType": "StripedWithParity",
"CapacityBytes": 48395
}
]
}

View File

@ -717,3 +717,76 @@ Storage resource it belongs to.
"SerialNumber": "1234570",
...
}
Storage Volume resource
+++++++++++++++++++++++
The *Volume* resource is emulated as a persistent emulator database
record, backed by the libvirt virtualization backend of the dynamic
Redfish emulator.
Only the volumes specified in the config file or created via a POST request
are allowed to be emulated upon by the emulator and appear as libvirt volumes
in the libvirt virtualization backend. Volumes other than these can neither be
listed nor deleted.
To allow libvirt volumes to be emulated upon, they need to be specified
in the configuration file in the following format (keyed compositely by
the System UUID and the Storage ID):
.. code-block:: python
SUSHY_EMULATOR_VOLUMES = {
('da69abcc-dae0-4913-9a7b-d344043097c0', '1'): [
{
"libvirtPoolName": "sushyPool",
"libvirtVolName": "testVol",
"Id": "1",
"Name": "Sample Volume 1",
"VolumeType": "Mirrored",
"CapacityBytes": 23748
},
{
"libvirtPoolName": "sushyPool",
"libvirtVolName": "testVol1",
"Id": "2",
"Name": "Sample Volume 2",
"VolumeType": "StripedWithParity",
"CapacityBytes": 48395
}
]
}
The Volume resources can be revealed by querying Volumes resource
for the corresponding System and the Storage.
.. code-block:: bash
curl http://localhost:8000/redfish/v1/Systems/da69abcc-dae0-4913-9a7b-d344043097c0/Storage/1/Volumes
{
"@odata.type": "#VolumeCollection.VolumeCollection",
"Name": "Storage Volume Collection",
"Members@odata.count": 2,
"Members": [
{
"@odata.id": "/redfish/v1/Systems/da69abcc-dae0-4913-9a7b-d344043097c0/Storage/1/Volumes/1"
},
{
"@odata.id": "/redfish/v1/Systems/da69abcc-dae0-4913-9a7b-d344043097c0/Storage/1/Volumes/2"
}
],
"@odata.context": "/redfish/v1/$metadata#VolumeCollection.VolumeCollection",
"@odata.id": "/redfish/v1/Systems/da69abcc-dae0-4913-9a7b-d344043097c0/Storage/1/Volumes",
}
A new volume can also be created in the libvirt backend via a POST request
on a Volume Collection:
.. code-block:: bash
curl -d '{"Name": "SampleVol",\
"VolumeType": "Mirrored",\
"CapacityBytes": 74859}' \
-H "Content-Type: application/json" \
-X POST \
http://localhost:8000/redfish/v1/Systems/da69abcc-dae0-4913-9a7b-d344043097c0/Storage/1/Volumes

View File

@ -0,0 +1,13 @@
---
features:
- |
Adds Volume resource emulation support.
As of this release, a user can configure a collection of Volumes including
the VolumeType and Capacity. The configured volumes will appear as libvirt
volumes in the libvirt virtualization backend of the dynamic Redfish
emulator (provided the libvirt pool specified for the volume exists).
Volume creation via POST request is also supported.
In case the Openstack backend is used, the NotSupportedError is raised.

View File

@ -31,6 +31,7 @@ from sushy_tools.emulator.resources.storage import staticdriver as stgdriver
from sushy_tools.emulator.resources.systems import libvirtdriver
from sushy_tools.emulator.resources.systems import novadriver
from sushy_tools.emulator.resources.vmedia import staticdriver as vmddriver
from sushy_tools.emulator.resources.volumes import staticdriver as voldriver
from sushy_tools import error
from sushy_tools.error import FishyError
@ -48,6 +49,7 @@ class Resources(object):
VMEDIA = None
STORAGE = None
DRIVES = None
VOLUMES = None
def __new__(cls, *args, **kwargs):
@ -129,6 +131,13 @@ class Resources(object):
'Initialized drive resource backed by %s '
'driver', cls.DRIVES().driver)
if cls.VOLUMES is None:
cls.VOLUMES = voldriver.StaticDriver.initialize(app.config)
app.logger.debug(
'Initialized volumes resource backed by %s '
'driver', cls.VOLUMES().driver)
return super(Resources, cls).__new__(cls, *args, **kwargs)
def __enter__(self):
@ -139,6 +148,7 @@ class Resources(object):
self.vmedia = self.VMEDIA()
self.storage = self.STORAGE()
self.drives = self.DRIVES()
self.volumes = self.VOLUMES()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
@ -149,6 +159,7 @@ class Resources(object):
del self.vmedia
del self.storage
del self.drives
del self.volumes
def instance_denied(**kwargs):
@ -734,6 +745,72 @@ def drive_resource(identity, stg_id, drv_id):
return 'Not found', 404
@app.route('/redfish/v1/Systems/<identity>/Storage/<storage_id>/Volumes',
methods=['GET', 'POST'])
@ensure_instance_access
@returns_json
def volumes_collection(identity, storage_id):
with Resources() as resources:
uuid = resources.systems.uuid(identity)
if flask.request.method == 'GET':
vol_col = resources.volumes.get_volumes_col(uuid, storage_id)
vol_ids = []
for vol in vol_col:
vol_id = resources.systems.find_or_create_storage_volume(vol)
if not vol_id:
resources.volumes.delete_volume(uuid, storage_id, vol)
else:
vol_ids.append(vol_id)
return flask.render_template(
'volume_collection.json', identity=identity,
storage_id=storage_id, volume_col=vol_ids)
elif flask.request.method == 'POST':
data = {
"Name": flask.request.json.get('Name'),
"VolumeType": flask.request.json.get('VolumeType'),
"CapacityBytes": flask.request.json.get('CapacityBytes'),
"Id": str(os.getpid()) + datetime.now().strftime("%H%M%S")
}
data['libvirtVolName'] = data['Id']
new_id = resources.systems.find_or_create_storage_volume(data)
if new_id:
resources.volumes.add_volume(uuid, storage_id, data)
app.logger.debug('New storage volume created with ID "%s"',
new_id)
vol_url = ("/redfish/v1/Systems/%s/Storage/%s/"
"Volumes/%s" % (identity, storage_id, new_id))
return flask.Response(status=201,
headers={'Location': vol_url})
@app.route('/redfish/v1/Systems/<identity>/Storage/<stg_id>/Volumes/<vol_id>',
methods=['GET'])
@ensure_instance_access
@returns_json
def volume(identity, stg_id, vol_id):
with Resources() as resources:
uuid = resources.systems.uuid(identity)
vol_col = resources.volumes.get_volumes_col(uuid, stg_id)
for vol in vol_col:
if vol['Id'] == vol_id:
vol_id = resources.systems.find_or_create_storage_volume(vol)
if not vol_id:
resources.volumes.delete_volume(uuid, stg_id, vol)
else:
return flask.render_template(
'volume.json', identity=identity, storage_id=stg_id,
volume=vol)
return 'Not Found', 404
def parse_args():
parser = argparse.ArgumentParser('sushy-emulator')
parser.add_argument('--config',

View File

@ -199,3 +199,13 @@ class AbstractSystemsDriver(DriverBase):
:returns: dict of Simple Storage Controllers and their atributes
"""
def find_or_create_storage_volume(self, data):
"""Find/create volume based on existence in the virtualization backend
:param data: data about the volume in dict form with values for `Id`,
`Name`, `CapacityBytes`, `VolumeType`, `libvirtPoolName`
and `libvirtVolName`
:returns: Id of the volume if successfully found/created else None
"""

View File

@ -125,7 +125,7 @@ class LibvirtDriver(AbstractSystemsDriver):
STORAGE_VOLUME_XML = """
<volume type='file'>
<name>%(name)s</name>
<key>%(name)s</key>
<key>%(path)s</key>
<capacity unit='bytes'>%(size)i</capacity>
<physical unit='bytes'>%(size)i</physical>
<target>
@ -933,3 +933,60 @@ class LibvirtDriver(AbstractSystemsDriver):
simple_storage[ctl_type]['Name'] = ctl_type
simple_storage[ctl_type]['DeviceList'].append(disk_device)
return simple_storage
def find_or_create_storage_volume(self, data):
"""Find/create volume based on existence in the virtualization backend
:param data: data about the volume in dict form with values for `Id`,
`Name`, `CapacityBytes`, `VolumeType`, `libvirtPoolName`
and `libvirtVolName`
:returns: Id of the volume if successfully found/created else None
"""
with libvirt_open(self._uri) as conn:
try:
poolName = data['libvirtPoolName']
except KeyError:
poolName = self.STORAGE_POOL
try:
pool = conn.storagePoolLookupByName(poolName)
except libvirt.libvirtError as ex:
msg = ('Error finding Storage Pool by name "%(name)s" at '
'libvirt URI "%(uri)s": %(err)s' %
{'name': poolName, 'uri': self._uri, 'err': ex})
logger.debug(msg)
return
try:
vol = pool.storageVolLookupByName(data['libvirtVolName'])
except libvirt.libvirtError as ex:
msg = ('Creating storage volume with name: "%s"',
data['libvirtVolName'])
logger.debug(msg)
pool_tree = ET.fromstring(pool.XMLDesc())
# Find out path to the volume
pool_path_element = pool_tree.find('target/path')
if pool_path_element is None:
msg = ('Missing "target/path" tag in the libvirt '
'storage pool "%(pool)s"'
'' % {'pool': poolName})
logger.debug(msg)
return
vol_path = os.path.join(
pool_path_element.text, data['libvirtVolName'])
# Create a new volume
vol = pool.createXML(
self.STORAGE_VOLUME_XML % {
'name': data['libvirtVolName'], 'path': vol_path,
'size': data['CapacityBytes']})
if not vol:
msg = ('Error creating "%s" storage volume in "%s" pool',
data['libvirtVolName'], poolName)
logger.debug(msg)
return
return data['Id']

View File

@ -374,3 +374,14 @@ class OpenStackDriver(AbstractSystemsDriver):
def get_simple_storage_collection(self, identity):
raise error.NotSupportedError('Not implemented')
def find_or_create_storage_volume(self, data):
"""Find/create volume based on existence in the virtualization backend
:param data: data about the volume in dict form with values for `Id`,
`Name`, `CapacityBytes`, `VolumeType`, `libvirtPoolName`
and `libvirtVolName`
:returns: Id of the volume if successfully found/created else None
"""
raise error.NotSupportedError('Not implemented')

View File

@ -0,0 +1,81 @@
# Copyright 2019 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import logging
from sushy_tools.emulator import memoize
from sushy_tools.emulator.resources.base import DriverBase
import uuid
logger = logging.getLogger(__name__)
class StaticDriver(DriverBase):
"""Redfish Volumes emulated in libvirt backed by the config file
Maintains the libvirt volumes in memory.
"""
@classmethod
def initialize(cls, config):
cls._config = config
cls._volumes = memoize.PersistentDict()
if hasattr(cls._volumes, 'make_permanent'):
cls._volumes.make_permanent(
config.get('SUSHY_EMULATOR_STATE_DIR'), 'volumes')
cls._volumes.update(
config.get('SUSHY_EMULATOR_VOLUMES', {}))
return cls
@property
def driver(self):
"""Return human-friendly driver information
:returns: driver information as `str`
"""
return '<static-volumes>'
def get_volumes_col(self, identity, storage_id):
try:
uu_identity = str(uuid.UUID(identity))
return self._volumes[(uu_identity, storage_id)]
except (KeyError, ValueError):
msg = ('Error finding volume collection by System UUID %s '
'and Storage ID %s' % (uu_identity, storage_id))
logger.debug(msg)
def add_volume(self, uu_identity, storage_id, vol):
if not self._volumes[(uu_identity, storage_id)]:
self._volumes[(uu_identity, storage_id)] = []
vol_col = self._volumes[(uu_identity, storage_id)]
vol_col.append(vol)
self._volumes.update({(uu_identity, storage_id): vol_col})
def delete_volume(self, uu_identity, storage_id, vol):
try:
vol_col = self._volumes[(uu_identity, storage_id)]
except KeyError:
msg = ('Error finding volume collection by System UUID %s '
'and Storage ID %s' % (uu_identity, storage_id))
logger.debug(msg)
else:
vol_col.remove(vol)
self._volumes.update({(uu_identity, storage_id): vol_col})

View File

@ -0,0 +1,16 @@
{
"@odata.type": "#Volume.v1_0_3.Volume",
"Id": {{ volume['Id']|string|tojson }},
"Name": {{ volume['Name']|string|tojson }},
"Status": {
"@odata.type": "#Resource.Status",
"State": "Enabled",
"Health": "OK"
},
"Encrypted": false,
"VolumeType": {{ volume['VolumeType']|string|tojson }},
"CapacityBytes": {{ volume['CapacityBytes'] }},
"@odata.context": "/redfish/v1/$metadata#Volume.Volume",
"@odata.id": {{ "/redfish/v1/Systems/%s/Storage/%s/Volumes/%s"|format(identity, storage_id, volume['Id'])|tojson }},
"@Redfish.Copyright": "Copyright 2014-2017 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
}

View File

@ -0,0 +1,15 @@
{
"@odata.type": "#VolumeCollection.VolumeCollection",
"Name": "Storage Volume Collection",
"Members@odata.count": {{ volume_col|length }},
"Members": [
{% for volume in volume_col %}
{
"@odata.id": {{ "/redfish/v1/Systems/%s/Storage/%s/Volumes/%s"|format(identity, storage_id, volume)|tojson }}
}{% if not loop.last %},{% endif %}
{% endfor %}
],
"@odata.context": "/redfish/v1/$metadata#VolumeCollection.VolumeCollection",
"@odata.id": {{ "/redfish/v1/Systems/%s/Storage/%s/Volumes"|format(identity, storage_id)|tojson }},
"@Redfish.Copyright": "Copyright 2014-2017 Distributed Management Task Force, Inc. (DMTF). For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
}

View File

@ -645,3 +645,24 @@ class LibvirtDriverTestCase(base.BaseTestCase):
.get_simple_storage_collection(self.uuid))
self.assertEqual({}, simple_storage_response)
@mock.patch('libvirt.open', autospec=True)
def test_find_or_create_storage_volume(self, libvirt_mock):
conn_mock = libvirt_mock.return_value
vol_data = {
"libvirtVolName": "123456",
"Id": "1",
"Name": "Sample Vol",
"CapacityBytes": 12345,
"VolumeType": "Mirrored"
}
pool_mock = conn_mock.storagePoolLookupByName.return_value
with open('sushy_tools/tests/unit/emulator/pool.xml', 'r') as f:
data = f.read()
pool_mock.storageVolLookupByName.side_effect = libvirt.libvirtError(
'Storage volume not found')
pool_mock.XMLDesc.return_value = data
self.test_driver.find_or_create_storage_volume(vol_data)
pool_mock.createXML.assert_called_once_with(mock.ANY)

View File

@ -0,0 +1,86 @@
# Copyright 2019 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslotest import base
from six.moves import mock
from sushy_tools.emulator.resources.volumes.staticdriver import StaticDriver
@mock.patch('sushy_tools.emulator.resources.volumes'
'.staticdriver.memoize.PersistentDict', new=dict)
class StaticDriverTestCase(base.BaseTestCase):
SYSTEM_UUID = "da69abcc-dae0-4913-9a7b-d344043097c0"
STORAGE_ID = "1"
VOLUMES_COL = [
{
"libvirtPoolName": "sushyPool",
"libvirtVolName": "testVol",
"Id": "1",
"Name": "Sample Volume 1",
"VolumeType": "Mirrored",
"CapacityBytes": 23748
},
{
"libvirtPoolName": "sushyPool",
"libvirtVolName": "testVol1",
"Id": "2",
"Name": "Sample Volume 2",
"VolumeType": "StripedWithParity",
"CapacityBytes": 48395
}
]
CONFIG = {
'SUSHY_EMULATOR_VOLUMES': {
(SYSTEM_UUID, STORAGE_ID): VOLUMES_COL
}
}
def test_get_volumes_col(self):
test_driver = StaticDriver.initialize(self.CONFIG)()
vol_col = test_driver.get_volumes_col(self.SYSTEM_UUID,
self.STORAGE_ID)
self.assertEqual(self.VOLUMES_COL, vol_col)
def test_add_volume(self):
test_driver = StaticDriver.initialize(self.CONFIG)()
vol = {
"libvirtPoolName": "sushyPool",
"libvirtVolName": "testVol2",
"Id": "3",
"Name": "Sample Volume 3",
"VolumeType": "Mirrored",
"CapacityBytes": 76584
}
test_driver.add_volume(self.SYSTEM_UUID, self.STORAGE_ID, vol)
vol_col = test_driver.get_volumes_col(self.SYSTEM_UUID,
self.STORAGE_ID)
self.assertTrue(vol in vol_col)
def test_delete_volume(self):
test_driver = StaticDriver.initialize(self.CONFIG)()
vol = {
"libvirtPoolName": "sushyPool",
"libvirtVolName": "testVol",
"Id": "1",
"Name": "Sample Volume 1",
"VolumeType": "Mirrored",
"CapacityBytes": 23748
}
test_driver.delete_volume(self.SYSTEM_UUID, self.STORAGE_ID, vol)
vol_col = test_driver.get_volumes_col(self.SYSTEM_UUID,
self.STORAGE_ID)
self.assertFalse(vol in vol_col)

View File

@ -783,3 +783,63 @@ class EmulatorTestCase(base.BaseTestCase):
self.assertEqual('Drive Sample', response.json['Name'])
self.assertEqual(899527000000, response.json['CapacityBytes'])
self.assertEqual('SAS', response.json['Protocol'])
def test_volume_collection_get(self, resources_mock):
resources_mock = resources_mock.return_value.__enter__.return_value
resources_mock.volumes.get_volumes_col.return_value = [
{
"libvirtPoolName": "sushyPool",
"libvirtVolName": "testVol",
"Id": "1",
"Name": "Sample Volume 1",
"VolumeType": "Mirrored",
"CapacityBytes": 23748
}
]
resources_mock.systems.find_or_create_storage_volume.return_value = "1"
response = self.app.get('/redfish/v1/Systems/vmc-node/Storage/1/'
'Volumes')
self.assertEqual(200, response.status_code)
self.assertEqual({'@odata.id':
'/redfish/v1/Systems/vmc-node/Storage/1/Volumes/1'},
response.json['Members'][0])
def test_create_volume_post(self, resources_mock):
resources_mock = resources_mock.return_value.__enter__.return_value
systems_mock = resources_mock.systems
systems_mock.find_or_create_storage_volume.return_value = "13087010612"
data = {
"Name": "Sample Volume 3",
"VolumeType": "NonRedundant",
"CapacityBytes": 23456
}
response = self.app.post('/redfish/v1/Systems/vmc-node/Storage/1/'
'Volumes', json=data)
self.assertEqual(201, response.status_code)
self.assertEqual('http://localhost/redfish/v1/Systems/vmc-node/'
'Storage/1/Volumes/13087010612',
response.headers['Location'])
def test_volume_resource_get(self, resources_mock):
resources_mock = resources_mock.return_value.__enter__.return_value
resources_mock.volumes.get_volumes_col.return_value = [
{
"libvirtPoolName": "sushyPool",
"libvirtVolName": "testVol",
"Id": "1",
"Name": "Sample Volume 1",
"VolumeType": "Mirrored",
"CapacityBytes": 23748
}
]
resources_mock.systems.find_or_create_storage_volume.return_value = "1"
response = self.app.get('/redfish/v1/Systems/vbmc-node/Storage/1/'
'Volumes/1')
self.assertEqual(200, response.status_code)
self.assertEqual('1', response.json['Id'])
self.assertEqual('Sample Volume 1', response.json['Name'])
self.assertEqual('Mirrored', response.json['VolumeType'])
self.assertEqual(23748, response.json['CapacityBytes'])