Merge "Add Volume resource support"
This commit is contained in:
commit
2169622852
@ -136,4 +136,39 @@ SUSHY_EMULATOR_DRIVES = {
|
|||||||
"Protocol": "SAS"
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
@ -716,3 +716,76 @@ Storage resource it belongs to.
|
|||||||
"SerialNumber": "1234570",
|
"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
|
||||||
|
13
releasenotes/notes/add-volume-resource-db795af928e41e5c.yaml
Normal file
13
releasenotes/notes/add-volume-resource-db795af928e41e5c.yaml
Normal 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.
|
@ -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 libvirtdriver
|
||||||
from sushy_tools.emulator.resources.systems import novadriver
|
from sushy_tools.emulator.resources.systems import novadriver
|
||||||
from sushy_tools.emulator.resources.vmedia import staticdriver as vmddriver
|
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 import error
|
||||||
from sushy_tools.error import FishyError
|
from sushy_tools.error import FishyError
|
||||||
|
|
||||||
@ -48,6 +49,7 @@ class Resources(object):
|
|||||||
VMEDIA = None
|
VMEDIA = None
|
||||||
STORAGE = None
|
STORAGE = None
|
||||||
DRIVES = None
|
DRIVES = None
|
||||||
|
VOLUMES = None
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
|
|
||||||
@ -129,6 +131,13 @@ class Resources(object):
|
|||||||
'Initialized drive resource backed by %s '
|
'Initialized drive resource backed by %s '
|
||||||
'driver', cls.DRIVES().driver)
|
'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)
|
return super(Resources, cls).__new__(cls, *args, **kwargs)
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
@ -139,6 +148,7 @@ class Resources(object):
|
|||||||
self.vmedia = self.VMEDIA()
|
self.vmedia = self.VMEDIA()
|
||||||
self.storage = self.STORAGE()
|
self.storage = self.STORAGE()
|
||||||
self.drives = self.DRIVES()
|
self.drives = self.DRIVES()
|
||||||
|
self.volumes = self.VOLUMES()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
@ -149,6 +159,7 @@ class Resources(object):
|
|||||||
del self.vmedia
|
del self.vmedia
|
||||||
del self.storage
|
del self.storage
|
||||||
del self.drives
|
del self.drives
|
||||||
|
del self.volumes
|
||||||
|
|
||||||
|
|
||||||
def instance_denied(**kwargs):
|
def instance_denied(**kwargs):
|
||||||
@ -734,6 +745,72 @@ def drive_resource(identity, stg_id, drv_id):
|
|||||||
return 'Not found', 404
|
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():
|
def parse_args():
|
||||||
parser = argparse.ArgumentParser('sushy-emulator')
|
parser = argparse.ArgumentParser('sushy-emulator')
|
||||||
parser.add_argument('--config',
|
parser.add_argument('--config',
|
||||||
|
@ -199,3 +199,13 @@ class AbstractSystemsDriver(DriverBase):
|
|||||||
|
|
||||||
:returns: dict of Simple Storage Controllers and their atributes
|
: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
|
||||||
|
"""
|
||||||
|
@ -125,7 +125,7 @@ class LibvirtDriver(AbstractSystemsDriver):
|
|||||||
STORAGE_VOLUME_XML = """
|
STORAGE_VOLUME_XML = """
|
||||||
<volume type='file'>
|
<volume type='file'>
|
||||||
<name>%(name)s</name>
|
<name>%(name)s</name>
|
||||||
<key>%(name)s</key>
|
<key>%(path)s</key>
|
||||||
<capacity unit='bytes'>%(size)i</capacity>
|
<capacity unit='bytes'>%(size)i</capacity>
|
||||||
<physical unit='bytes'>%(size)i</physical>
|
<physical unit='bytes'>%(size)i</physical>
|
||||||
<target>
|
<target>
|
||||||
@ -933,3 +933,60 @@ class LibvirtDriver(AbstractSystemsDriver):
|
|||||||
simple_storage[ctl_type]['Name'] = ctl_type
|
simple_storage[ctl_type]['Name'] = ctl_type
|
||||||
simple_storage[ctl_type]['DeviceList'].append(disk_device)
|
simple_storage[ctl_type]['DeviceList'].append(disk_device)
|
||||||
return simple_storage
|
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']
|
||||||
|
@ -374,3 +374,14 @@ class OpenStackDriver(AbstractSystemsDriver):
|
|||||||
|
|
||||||
def get_simple_storage_collection(self, identity):
|
def get_simple_storage_collection(self, identity):
|
||||||
raise error.NotSupportedError('Not implemented')
|
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')
|
||||||
|
0
sushy_tools/emulator/resources/volumes/__init__.py
Normal file
0
sushy_tools/emulator/resources/volumes/__init__.py
Normal file
81
sushy_tools/emulator/resources/volumes/staticdriver.py
Normal file
81
sushy_tools/emulator/resources/volumes/staticdriver.py
Normal 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})
|
16
sushy_tools/emulator/templates/volume.json
Normal file
16
sushy_tools/emulator/templates/volume.json
Normal 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."
|
||||||
|
}
|
15
sushy_tools/emulator/templates/volume_collection.json
Normal file
15
sushy_tools/emulator/templates/volume_collection.json
Normal 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."
|
||||||
|
}
|
@ -645,3 +645,24 @@ class LibvirtDriverTestCase(base.BaseTestCase):
|
|||||||
.get_simple_storage_collection(self.uuid))
|
.get_simple_storage_collection(self.uuid))
|
||||||
|
|
||||||
self.assertEqual({}, simple_storage_response)
|
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)
|
||||||
|
@ -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)
|
@ -783,3 +783,63 @@ class EmulatorTestCase(base.BaseTestCase):
|
|||||||
self.assertEqual('Drive Sample', response.json['Name'])
|
self.assertEqual('Drive Sample', response.json['Name'])
|
||||||
self.assertEqual(899527000000, response.json['CapacityBytes'])
|
self.assertEqual(899527000000, response.json['CapacityBytes'])
|
||||||
self.assertEqual('SAS', response.json['Protocol'])
|
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'])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user