Add OperationApplyTime support to Volume methods

The supported_values property in the OperationApplyTimeSupportField
class returns an unmapped list of OperationApplyTime values supported
by the service. The convention in Sushy for properties that return a
list of schema-defined enumeration values is to return a
MappedListField. This type of field performs mapping between Redfish
schema-defined enumeration values and constants exposed by the Sushy
package.

This update adds the mapped_supported_values property to return a
MappedListField of OperationApplyTime values supported by the service.
The supported_values property is deprecated.

The create_volume method in the VolumeCollection class and the
delete_volume and initialize_volume methods in the Volume class are
updated to take optional apply_time and timeout keyword parameters.
This allows the caller of those volume methods to specify a preferred
OperationApplyTime annotation and a maximum timeout for synchronous
operations. If the specified apply_time is 'Immediate', the operation
is called with blocking enabled. Otherwise blocking is disabled. For
asynchronous operations, those three methods will now return a
TaskMonitor instance that the caller can use to monitor the state of
the task.

Change-Id: I8ba2b9ff1e80fa0c2edc3a11ab80844d732e4394
Story: 2003514
Task: 41087
This commit is contained in:
Bill Dodd
2020-10-19 17:38:35 -05:00
parent 74b8111aff
commit f353550601
5 changed files with 188 additions and 22 deletions

View File

@@ -0,0 +1,19 @@
---
deprecations:
- |
The ``supported_values`` property in the
``OperationApplyTimeSupportField`` class is deprecated. Use the
``mapped_supported_values`` property instead. The
``mapped_supported_values`` property uses the ``MappedListField`` type
to map the Redfish schema-defined enumeration values to constants exposed
by the Sushy package.
features:
- |
Update the ``create_volume`` method in the ``VolumeCollection`` class and
the ``delete_volume`` and ``initialize_volume`` methods in the ``Volume``
class to take optional ``apply_time`` and ``timeout`` keyword parameters.
This allows the caller of those volume methods to specify a preferred
``OperationApplyTime`` annotation and a maximum timeout for synchronous
operations. For asynchronous operations, those three methods will now
return a ``TaskMonitor`` instance that the caller can use to monitor the
state of the task.

View File

@@ -42,8 +42,18 @@ class OperationApplyTimeSupportField(base.CompositeField):
supported_values = base.Field('SupportedValues', required=True,
adapter=list)
"""The client is allowed request when performing a create, delete, or
action operation"""
"""The types of apply times that the client is allowed request when
performing a create, delete, or action operation returned as an unmapped
list
Deprecated: Use `mapped_supported_values`.
"""
mapped_supported_values = base.MappedListField(
'SupportedValues', res_maps.APPLY_TIME_VALUE_MAP, required=True)
"""The types of apply times that the client is allowed request when
performing a create, delete, or action operation returned as a mapped
list"""
class ActionField(base.CompositeField):

View File

@@ -18,7 +18,11 @@ import logging
from sushy import exceptions
from sushy.resources import base
from sushy.resources import common
from sushy.resources import constants as res_cons
from sushy.resources import mappings as res_maps
from sushy.resources.system.storage import constants as store_cons
from sushy.resources.system.storage import mappings as store_maps
from sushy.resources.task_monitor import TaskMonitor
from sushy import utils
LOG = logging.getLogger(__name__)
@@ -86,30 +90,71 @@ class Volume(base.ResourceBase):
set(store_maps.VOLUME_INIT_TYPE_MAP).
intersection(action.allowed_values)])
def initialize_volume(self, value):
def initialize_volume(self, value=store_cons.VOLUME_INIT_TYPE_FAST,
apply_time=None, timeout=500):
"""Initialize the volume.
:param value: The InitializeType value.
:param apply_time: When to update the attributes. Optional.
APPLY_TIME_IMMEDIATE - Immediate,
APPLY_TIME_ON_RESET - On reset,
APPLY_TIME_MAINT_START - During specified maintenance time
APPLY_TIME_MAINT_RESET - On reset during specified maintenance time
:param timeout: Max time in seconds to wait for blocking async call.
:raises: InvalidParameterValueError, if the target value is not
allowed.
:raises: ConnectionError
:raises: HTTPError
:returns: TaskMonitor if async task or None if successful init
"""
valid_values = self.get_allowed_initialize_volume_values()
if value not in valid_values:
raise exceptions.InvalidParameterValueError(
parameter='value', value=value, valid_values=valid_values)
value = store_maps.VOLUME_INIT_TYPE_MAP_REV[value]
payload = {'InitializeType': value}
blocking = False
oat_prop = '@Redfish.OperationApplyTime'
if apply_time:
payload[oat_prop] = res_maps.APPLY_TIME_VALUE_MAP_REV[apply_time]
if (payload and payload.get(oat_prop) == res_maps.
APPLY_TIME_VALUE_MAP_REV[res_cons.APPLY_TIME_IMMEDIATE]):
blocking = True
target_uri = self._get_initialize_action_element().target_uri
self._conn.post(target_uri, data={'InitializeType': value},
blocking=True)
r = self._conn.post(target_uri, data=payload, blocking=blocking,
timeout=timeout)
if r.status_code == 202:
return (TaskMonitor(self, r.headers.get('location'))
.set_retry_after(r.headers.get('retry-after')))
def delete_volume(self, payload=None):
def delete_volume(self, payload=None, apply_time=None, timeout=500):
"""Delete the volume.
:param payload: May contain @Redfish.OperationApplyTime property
:param apply_time: When to update the attributes. Optional.
APPLY_TIME_IMMEDIATE - Immediate,
APPLY_TIME_ON_RESET - On reset,
APPLY_TIME_MAINT_START - During specified maintenance time
APPLY_TIME_MAINT_RESET - On reset during specified maintenance time
:param timeout: Max time in seconds to wait for blocking async call.
:raises: ConnectionError
:raises: HTTPError
:returns: TaskMonitor if async task or None if successful deletion
"""
self._conn.delete(self._path, data=payload, blocking=True)
blocking = False
oat_prop = '@Redfish.OperationApplyTime'
if apply_time:
if payload is None:
payload = {}
payload[oat_prop] = res_maps.APPLY_TIME_VALUE_MAP_REV[apply_time]
if (payload and payload.get(oat_prop) == res_maps.
APPLY_TIME_VALUE_MAP_REV[res_cons.APPLY_TIME_IMMEDIATE]):
blocking = True
r = self._conn.delete(self._path, data=payload, blocking=blocking,
timeout=timeout)
if r.status_code == 202:
return (TaskMonitor(self, r.headers.get('location'))
.set_retry_after(r.headers.get('retry-after')))
class VolumeCollection(base.ResourceCollectionBase):
@@ -145,17 +190,36 @@ class VolumeCollection(base.ResourceCollectionBase):
"""Indicates if a client is allowed to request for a specific apply
time of a create, delete, or action operation of a given resource"""
def create_volume(self, payload):
def create_volume(self, payload, apply_time=None, timeout=500):
"""Create a volume.
:param payload: The payload representing the new volume to create.
:param apply_time: When to update the attributes. Optional.
APPLY_TIME_IMMEDIATE - Immediate,
APPLY_TIME_ON_RESET - On reset,
APPLY_TIME_MAINT_START - During specified maintenance time
APPLY_TIME_MAINT_RESET - On reset during specified maintenance time
:param timeout: Max time in seconds to wait for blocking async call.
:raises: ConnectionError
:raises: HTTPError
:returns: Newly created Volume resource or None if no Location header
:returns: Newly created Volume resource or TaskMonitor if async task
"""
r = self._conn.post(self._path, data=payload, blocking=True)
blocking = False
oat_prop = '@Redfish.OperationApplyTime'
if apply_time:
if payload is None:
payload = {}
payload[oat_prop] = res_maps.APPLY_TIME_VALUE_MAP_REV[apply_time]
if (payload and payload.get(oat_prop) == res_maps.
APPLY_TIME_VALUE_MAP_REV[res_cons.APPLY_TIME_IMMEDIATE]):
blocking = True
r = self._conn.post(self._path, data=payload, blocking=blocking,
timeout=timeout)
location = r.headers.get('Location')
if r.status_code == 201:
if location:
self.refresh()
return self.get_member(location)
elif r.status_code == 202:
return (TaskMonitor(self, location)
.set_retry_after(r.headers.get('retry-after')))

View File

@@ -17,6 +17,8 @@ from dateutil import parser
import sushy
from sushy import exceptions
from sushy.resources import constants as res_cons
from sushy.resources.system.storage import constants as store_cons
from sushy.resources.system.storage import volume
from sushy.tests.unit import base
@@ -54,12 +56,27 @@ class VolumeTestCase(base.TestCase):
identifier.durable_name)
self.assertIsNone(self.stor_volume.block_size_bytes)
def test_initialize_volume(self):
def test_initialize_volume_immediate(self):
target_uri = '/redfish/v1/Systems/3/Storage/RAIDIntegrated/' \
'Volumes/1/Actions/Volume.Initialize'
self.stor_volume.initialize_volume('fast')
self.stor_volume.initialize_volume(
store_cons.VOLUME_INIT_TYPE_FAST,
apply_time=res_cons.APPLY_TIME_IMMEDIATE)
self.stor_volume._conn.post.assert_called_once_with(
target_uri, data={'InitializeType': 'Fast'}, blocking=True)
target_uri, data={'InitializeType': 'Fast',
'@Redfish.OperationApplyTime': 'Immediate'},
blocking=True, timeout=500)
def test_initialize_volume_on_reset(self):
target_uri = '/redfish/v1/Systems/3/Storage/RAIDIntegrated/' \
'Volumes/1/Actions/Volume.Initialize'
self.stor_volume.initialize_volume(
store_cons.VOLUME_INIT_TYPE_FAST,
apply_time=res_cons.APPLY_TIME_ON_RESET)
self.stor_volume._conn.post.assert_called_once_with(
target_uri, data={'InitializeType': 'Fast',
'@Redfish.OperationApplyTime': 'OnReset'},
blocking=False, timeout=500)
def test_initialize_volume_bad_value(self):
self.assertRaisesRegex(
@@ -70,13 +87,28 @@ class VolumeTestCase(base.TestCase):
def test_delete_volume(self):
self.stor_volume.delete_volume()
self.stor_volume._conn.delete.assert_called_once_with(
self.stor_volume._path, data=None, blocking=True)
self.stor_volume._path, data=None, blocking=False, timeout=500)
def test_delete_volume_with_payload(self):
payload = {'@Redfish.OperationApplyTime': 'OnReset'}
payload = {'@Redfish.OperationApplyTime': 'Immediate'}
self.stor_volume.delete_volume(payload=payload)
self.stor_volume._conn.delete.assert_called_once_with(
self.stor_volume._path, data=payload, blocking=True)
self.stor_volume._path, data=payload, blocking=True, timeout=500)
def test_delete_volume_immediate(self):
payload = {}
self.stor_volume.delete_volume(
payload=payload, apply_time=res_cons.APPLY_TIME_IMMEDIATE)
self.stor_volume._conn.delete.assert_called_once_with(
self.stor_volume._path, data=payload, blocking=True, timeout=500)
def test_delete_volume_on_reset(self):
payload = {}
self.stor_volume.delete_volume(
payload=payload, apply_time=res_cons.APPLY_TIME_ON_RESET,
timeout=250)
self.stor_volume._conn.delete.assert_called_once_with(
self.stor_volume._path, data=payload, blocking=False, timeout=250)
class VolumeCollectionTestCase(base.TestCase):
@@ -113,6 +145,10 @@ class VolumeCollectionTestCase(base.TestCase):
support._maintenance_window_resource.resource_uri)
self.assertEqual(['Immediate', 'OnReset', 'AtMaintenanceWindowStart'],
support.supported_values)
self.assertEqual([res_cons.APPLY_TIME_IMMEDIATE,
res_cons.APPLY_TIME_ON_RESET,
res_cons.APPLY_TIME_MAINT_START],
support.mapped_supported_values)
@mock.patch.object(volume, 'Volume', autospec=True)
def test_get_member(self, Volume_mock):
@@ -175,23 +211,54 @@ class VolumeCollectionTestCase(base.TestCase):
self.assertEqual(1073741824000, self.stor_vol_col.max_size_bytes)
def test_create_volume(self):
def test_create_volume_immediate(self):
payload = {
'Name': 'My Volume 4',
'VolumeType': 'Mirrored',
'RAIDType': 'RAID1',
'CapacityBytes': 107374182400
}
expected_payload = dict(payload)
expected_payload['@Redfish.OperationApplyTime'] = 'Immediate'
with open('sushy/tests/unit/json_samples/volume4.json') as f:
self.conn.get.return_value.json.return_value = json.load(f)
self.conn.post.return_value.status_code = 201
self.conn.post.return_value.headers.return_value = {
'Location': '/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/4'
}
new_vol = self.stor_vol_col.create_volume(payload)
new_vol = self.stor_vol_col.create_volume(
payload, apply_time=res_cons.APPLY_TIME_IMMEDIATE)
self.stor_vol_col._conn.post.assert_called_once_with(
'/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes',
data=payload, blocking=True)
data=expected_payload, blocking=True, timeout=500)
self.stor_vol_col.refresh.assert_called_once()
self.assertIsNotNone(new_vol)
self.assertEqual('4', new_vol.identity)
self.assertEqual('My Volume 4', new_vol.name)
self.assertEqual(107374182400, new_vol.capacity_bytes)
self.assertEqual(sushy.VOLUME_TYPE_MIRRORED, new_vol.volume_type)
self.assertEqual(sushy.RAID_TYPE_RAID1, new_vol.raid_type)
def test_create_volume_on_reset(self):
payload = {
'Name': 'My Volume 4',
'VolumeType': 'Mirrored',
'RAIDType': 'RAID1',
'CapacityBytes': 107374182400
}
expected_payload = dict(payload)
expected_payload['@Redfish.OperationApplyTime'] = 'OnReset'
with open('sushy/tests/unit/json_samples/volume4.json') as f:
self.conn.get.return_value.json.return_value = json.load(f)
self.conn.post.return_value.status_code = 201
self.conn.post.return_value.headers.return_value = {
'Location': '/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes/4'
}
new_vol = self.stor_vol_col.create_volume(
payload, apply_time=res_cons.APPLY_TIME_ON_RESET)
self.stor_vol_col._conn.post.assert_called_once_with(
'/redfish/v1/Systems/437XR1138R2/Storage/1/Volumes',
data=expected_payload, blocking=False, timeout=500)
self.stor_vol_col.refresh.assert_called_once()
self.assertIsNotNone(new_vol)
self.assertEqual('4', new_vol.identity)

View File

@@ -107,9 +107,12 @@ class SystemTestCase(base.TestCase):
'/redfish/v1/Systems/437XR1138R2'},
'maintenance_window_duration_in_seconds': 600,
'maintenance_window_start_time':
parser.parse('2017-05-03T23:12:37-05:00'),
'supported_values':
['Immediate', 'AtMaintenanceWindowStart']},
parser.parse('2017-05-03T23:12:37-05:00'),
'supported_values':
['Immediate', 'AtMaintenanceWindowStart'],
'mapped_supported_values':
[res_cons.APPLY_TIME_IMMEDIATE,
res_cons.APPLY_TIME_MAINT_START]},
'target_uri':
'/redfish/v1/Systems/437XR1138R2/Actions/'
'ComputerSystem.Reset'}},
@@ -206,6 +209,9 @@ class SystemTestCase(base.TestCase):
self.assertIsNotNone(support)
self.assertEqual(['Immediate', 'AtMaintenanceWindowStart'],
support.supported_values)
self.assertEqual([res_cons.APPLY_TIME_IMMEDIATE,
res_cons.APPLY_TIME_MAINT_START],
support.mapped_supported_values)
self.assertEqual(parser.parse('2017-05-03T23:12:37-05:00'),
support.maintenance_window_start_time)
self.assertEqual(600, support.maintenance_window_duration_in_seconds)