Fix missing ETag when patching Redfish resource

Some Redfish implementations require current ETag of Redfish
resource against which resource update is requested to be
included in HTTP PATCH operation header.
This commit adds missing ETag in PATCH operation header.

Change-Id: I3f88d8a7032fd9071aa033c706768127da1d12af
This commit is contained in:
Vanou Ishii 2023-07-18 19:08:11 -04:00
parent f03d77ebcb
commit 4ff512a6ee
17 changed files with 81 additions and 25 deletions

View File

@ -0,0 +1,6 @@
---
fixes:
- |
Fixes missing ETag in PATCH operation against Redfish resources with
backward compatibility for Redfish implementation which doesn't work
with ETag in header.

View File

@ -214,9 +214,10 @@ class Chassis(base.ResourceBase):
parameter='state', value=state,
valid_values=' ,'.join(i.value for i in res_cons.IndicatorLED))
etag = self._get_etag()
data = {'IndicatorLED': state}
self._conn.patch(self.path, data=data)
self._conn.patch(self.path, data=data, etag=etag)
self.invalidate()
@property

View File

@ -248,8 +248,10 @@ class VirtualMedia(base.ResourceBase):
parameter='verify_certificate', value=verify_certificate,
valid_values='boolean (True, False)')
etag = self._get_etag()
self._conn.patch(self.path,
data={'VerifyCertificate': verify_certificate})
data={'VerifyCertificate': verify_certificate},
etag=etag)
self.invalidate()
@property

View File

@ -170,7 +170,7 @@ class SettingsField(base.CompositeField):
:returns: Response object
"""
return connector.patch(self.resource_uri, data=value)
return connector.patch(self.resource_uri, data=value, etag=self._etag)
@property
def resource_uri(self):

View File

@ -149,6 +149,10 @@ class Bios(base.ResourceBase):
payload = utils.process_apply_time_input(
payload, apply_time, maint_window_start_time,
maint_window_duration)
# NOTE(vanou): To retrieve current ETag value of @Redfish.Settings
# but not update cached _pending_settings_resource, because cached
# property is only this one and re-cache is not required
self.refresh(force=False)
self._settings.commit(self._conn,
payload)
utils.cache_clear(self, force_refresh=False,

View File

@ -144,4 +144,6 @@ class SecureBoot(base.ResourceBase):
raise exceptions.InvalidParameterValueError(
"Expected a boolean for 'enabled', got %r" % enabled)
self._conn.patch(self.path, data={'SecureBootEnable': enabled})
etag = self._get_etag()
self._conn.patch(self.path, data={'SecureBootEnable': enabled},
etag=etag)

View File

@ -97,6 +97,10 @@ class StorageController(base.ResourceBase):
payload = utils.process_apply_time_input(
payload, apply_time, maint_window_start_time,
maint_window_duration)
# NOTE(vanou): To retrieve current ETag value of @Redfish.Settings
# but not update cached pending_settings, because cached property is
# only this one and re-cache this is not required
self.refresh(force=False)
r = self._settings.commit(self._conn, payload)
utils.cache_clear(self, force_refresh=False,
only_these=['pending_settings'])

View File

@ -102,7 +102,8 @@ class Drive(base.ResourceBase):
parameter='state', value=state,
valid_values=' ,'.join(i.value for i in res_cons.IndicatorLED))
etag = self._get_etag()
data = {'IndicatorLED': state}
self._conn.patch(self.path, data=data)
self._conn.patch(self.path, data=data, etag=etag)
self.invalidate()

View File

@ -342,9 +342,10 @@ class System(base.ResourceBase):
parameter='state', value=state,
valid_values=' ,'.join(i.value for i in res_cons.IndicatorLED))
etag = self._get_etag()
data = {'IndicatorLED': state}
self._conn.patch(self.path, data=data)
self._conn.patch(self.path, data=data, etag=etag)
self.invalidate()
def _get_processor_collection_path(self):

View File

@ -34,6 +34,7 @@ class ChassisTestCase(base.TestCase):
self.json_doc = json.load(f)
self.conn.get.return_value.json.return_value = self.json_doc
self.conn.get.return_value.headers = {'ETag': 'd37f7bcd528e4d59'}
self.chassis = chassis.Chassis(self.conn, '/redfish/v1/Chassis/Blade1',
redfish_version='1.8.0')
@ -144,7 +145,8 @@ class ChassisTestCase(base.TestCase):
self.chassis.set_indicator_led(sushy.IndicatorLED.BLINKING)
self.chassis._conn.patch.assert_called_once_with(
'/redfish/v1/Chassis/Blade1',
data={'IndicatorLED': 'Blinking'})
data={'IndicatorLED': 'Blinking'},
etag='d37f7bcd528e4d59')
invalidate_mock.assert_called_once_with()

View File

@ -294,13 +294,16 @@ class VirtualMediaTestCase(base.TestCase):
self.assertTrue(self.sys_virtual_media._is_stale)
def test_set_verify_certificate(self):
self.conn.get.return_value.headers = {'Allow': 'GET,HEAD',
'ETag': '3d7b8a7360bf2941d'}
with mock.patch.object(
self.sys_virtual_media, 'invalidate',
autospec=True) as invalidate_mock:
self.sys_virtual_media.set_verify_certificate(True)
self.sys_virtual_media._conn.patch.assert_called_once_with(
"/redfish/v1/Managers/BMC/VirtualMedia/Floppy1",
data={'VerifyCertificate': True})
data={'VerifyCertificate': True},
etag='3d7b8a7360bf2941d')
invalidate_mock.assert_called_once_with()

View File

@ -65,6 +65,8 @@ class ControllerTestCase(base.TestCase):
self.controller.supported_apply_times)
def test_update(self):
self.conn.get.return_value.json.side_effect = [
self.json_doc, self.json_doc]
mock_response = mock.Mock()
mock_response.status_code = http_client.ACCEPTED
mock_response.headers = {'Content-Length': 42,
@ -81,7 +83,8 @@ class ControllerTestCase(base.TestCase):
data={'ControllerRates': {'ConsistencyCheckRatePercent': 30},
'@Redfish.SettingsApplyTime': {
'@odata.type': '#Settings.v1_0_0.PreferredApplyTime',
'ApplyTime': 'OnReset'}})
'ApplyTime': 'OnReset'}},
etag=None)
self.assertIsInstance(tm, taskmonitor.TaskMonitor)
self.assertEqual('/Task/545', tm.task_monitor_uri)

View File

@ -83,13 +83,15 @@ class DriveTestCase(base.TestCase):
self.assertEqual('3', volumes[1].identity)
def test_set_indicator_led(self):
self.conn.get.return_value.headers = {'ETag': 'a3b01b63f80a4913'}
with mock.patch.object(
self.stor_drive, 'invalidate',
autospec=True) as invalidate_mock:
self.stor_drive.set_indicator_led(sushy.IndicatorLED.BLINKING)
self.stor_drive._conn.patch.assert_called_once_with(
'/redfish/v1/Systems/437XR1138/Storage/1/Drives/'
'32ADF365C6C1B7BD', data={'IndicatorLED': 'Blinking'})
'32ADF365C6C1B7BD', data={'IndicatorLED': 'Blinking'},
etag='a3b01b63f80a4913')
invalidate_mock.assert_called_once_with()

View File

@ -119,6 +119,10 @@ class BiosTestCase(base.TestCase):
attributes.get('maintenance_window'))
def test_set_attribute_apply_time(self):
self.conn.get.return_value.json.side_effect = [
self.bios_json,
self.bios_json]
self.sys_bios.set_attribute(
'ProcTurboMode', 'Disabled',
res_cons.ApplyTime.IN_MAINTENANCE_WINDOW_ON_RESET,
@ -131,9 +135,15 @@ class BiosTestCase(base.TestCase):
'@odata.type': '#Settings.v1_0_0.PreferredApplyTime',
'ApplyTime': 'InMaintenanceWindowOnReset',
'MaintenanceWindowStartTime': '2020-09-01T04:30:00',
'MaintenanceWindowDurationInSeconds': 600}})
'MaintenanceWindowDurationInSeconds': 600}},
etag='9234ac83b9700123cc32')
def test_set_attribute_on_refresh(self):
self.conn.get.return_value.json.side_effect = [
self.bios_settings_json,
self.bios_json,
self.bios_settings_json]
self.conn.get.reset_mock()
# make it to instantiate pending attributes
self.sys_bios.pending_attributes
@ -150,6 +160,9 @@ class BiosTestCase(base.TestCase):
self.assertTrue(self.conn.get.called)
def test_set_attributes(self):
self.conn.get.return_value.json.side_effect = [
self.bios_json]
self.sys_bios.set_attributes(
{'ProcTurboMode': 'Disabled', 'UsbControl': 'UsbDisabled'},
res_cons.ApplyTime.AT_MAINTENANCE_WINDOW_START,
@ -163,9 +176,15 @@ class BiosTestCase(base.TestCase):
'@odata.type': '#Settings.v1_0_0.PreferredApplyTime',
'ApplyTime': 'AtMaintenanceWindowStart',
'MaintenanceWindowStartTime': '2020-09-01T04:30:00',
'MaintenanceWindowDurationInSeconds': 600}})
'MaintenanceWindowDurationInSeconds': 600}},
etag='9234ac83b9700123cc32')
def test_set_attributes_on_refresh(self):
self.conn.get.return_value.json.side_effect = [
self.bios_settings_json,
self.bios_json,
self.bios_settings_json]
self.conn.get.reset_mock()
# make it to instantiate pending attributes
self.sys_bios.pending_attributes

View File

@ -29,6 +29,7 @@ class SecureBootTestCase(base.TestCase):
self.secure_boot_json = json.load(f)
self.conn.get.return_value.json.return_value = self.secure_boot_json
self.conn.get.return_value.headers = {'ETag': 'b26ae716a2c1f39f'}
self.secure_boot = secure_boot.SecureBoot(
self.conn, '/redfish/v1/Systems/437XR1138R2/SecureBoot',
registries={}, redfish_version='1.1.0')
@ -79,7 +80,8 @@ class SecureBootTestCase(base.TestCase):
self.secure_boot.set_enabled(True)
self.conn.patch.assert_called_once_with(
'/redfish/v1/Systems/437XR1138R2/SecureBoot',
data={'SecureBootEnable': True})
data={'SecureBootEnable': True},
etag='b26ae716a2c1f39f')
def test_set_enabled_wrong_type(self):
self.assertRaises(exceptions.InvalidParameterValueError,

View File

@ -46,6 +46,8 @@ class SystemTestCase(base.TestCase):
self.sys_inst = system.System(
self.conn, '/redfish/v1/Systems/437XR1138R2',
redfish_version='1.0.2')
self.sys_inst._get_etag = mock.Mock()
self.sys_inst._get_etag.return_value = '81802dbf61beb0bd'
def test__parse_attributes(self):
self.sys_inst._parse_attributes(self.json_doc)
@ -283,7 +285,7 @@ class SystemTestCase(base.TestCase):
data={'Boot': {'BootSourceOverrideEnabled': 'Continuous',
'BootSourceOverrideTarget': 'Pxe',
'BootSourceOverrideMode': 'UEFI'}},
etag=None)
etag='81802dbf61beb0bd')
def test_set_system_boot_options_no_mode_specified(self):
self.sys_inst.set_system_boot_options(
@ -293,7 +295,7 @@ class SystemTestCase(base.TestCase):
'/redfish/v1/Systems/437XR1138R2',
data={'Boot': {'BootSourceOverrideEnabled': 'Once',
'BootSourceOverrideTarget': 'Hdd'}},
etag=None)
etag='81802dbf61beb0bd')
def test_set_system_boot_options_no_target_specified(self):
self.sys_inst.set_system_boot_options(
@ -303,7 +305,7 @@ class SystemTestCase(base.TestCase):
'/redfish/v1/Systems/437XR1138R2',
data={'Boot': {'BootSourceOverrideEnabled': 'Continuous',
'BootSourceOverrideMode': 'UEFI'}},
etag=None)
etag='81802dbf61beb0bd')
def test_set_system_boot_options_no_freq_specified(self):
self.sys_inst.set_system_boot_options(
@ -313,7 +315,7 @@ class SystemTestCase(base.TestCase):
'/redfish/v1/Systems/437XR1138R2',
data={'Boot': {'BootSourceOverrideTarget': 'Pxe',
'BootSourceOverrideMode': 'UEFI'}},
etag=None)
etag='81802dbf61beb0bd')
def test_set_system_boot_options_nothing_specified(self):
self.sys_inst.set_system_boot_options()
@ -348,7 +350,7 @@ class SystemTestCase(base.TestCase):
'/redfish/v1/Systems/437XR1138R2',
data={'Boot': {'BootSourceOverrideEnabled': 'Once',
'BootSourceOverrideTarget': 'UsbCd'}},
etag=None)
etag='81802dbf61beb0bd')
def test_set_system_boot_options_supermicro_no_usb_cd_boot(self):
@ -361,7 +363,7 @@ class SystemTestCase(base.TestCase):
'/redfish/v1/Systems/437XR1138R2',
data={'Boot': {'BootSourceOverrideEnabled': 'Once',
'BootSourceOverrideTarget': 'Cd'}},
etag=None)
etag='81802dbf61beb0bd')
def test_set_system_boot_options_settings_resource_nokia(self):
with open('sushy/tests/unit/json_samples/settings-nokia.json') as f:
@ -487,10 +489,10 @@ class SystemTestCase(base.TestCase):
data={'Boot': {'BootSourceOverrideEnabled': 'Continuous',
'BootSourceOverrideTarget': 'Pxe',
'BootSourceOverrideMode': 'UEFI'}},
etag=None)
etag='81802dbf61beb0bd')
def test_set_system_boot_source_with_etag(self):
self.conn.get.return_value.headers = {'ETag': '"3d7b838291941d"'}
self.conn.get.return_value.headers = {'ETag': '"81802dbf61beb0bd"'}
self.sys_inst.set_system_boot_source(
sushy.BOOT_SOURCE_TARGET_PXE,
enabled=sushy.BOOT_SOURCE_ENABLED_CONTINUOUS,
@ -500,7 +502,7 @@ class SystemTestCase(base.TestCase):
data={'Boot': {'BootSourceOverrideEnabled': 'Continuous',
'BootSourceOverrideTarget': 'Pxe',
'BootSourceOverrideMode': 'UEFI'}},
etag='"3d7b838291941d"')
etag="81802dbf61beb0bd")
def test_set_system_boot_source_no_mode_specified(self):
self.sys_inst.set_system_boot_source(
@ -510,7 +512,7 @@ class SystemTestCase(base.TestCase):
'/redfish/v1/Systems/437XR1138R2',
data={'Boot': {'BootSourceOverrideEnabled': 'Once',
'BootSourceOverrideTarget': 'Hdd'}},
etag=None)
etag='81802dbf61beb0bd')
def test_set_system_boot_source_invalid_target(self):
self.assertRaises(exceptions.InvalidParameterValueError,
@ -533,7 +535,8 @@ class SystemTestCase(base.TestCase):
self.sys_inst.set_indicator_led(sushy.IndicatorLED.BLINKING)
self.sys_inst._conn.patch.assert_called_once_with(
'/redfish/v1/Systems/437XR1138R2',
data={'IndicatorLED': 'Blinking'})
data={'IndicatorLED': 'Blinking'},
etag='81802dbf61beb0bd')
invalidate_mock.assert_called_once_with()

View File

@ -78,7 +78,8 @@ class SettingsFieldTestCase(base.TestCase):
instance.commit(conn, {'Attributes': {'key': 'value'}})
conn.patch.assert_called_once_with(
'/redfish/v1/Systems/437XR1138R2/BIOS/Settings',
data={'Attributes': {'key': 'value'}})
data={'Attributes': {'key': 'value'}},
etag='9234ac83b9700123cc32')
def test_get_status_failure(self):
instance = self.settings._load(self.json, mock.Mock())