From 81352b1d34e2908ef2a314d5291e582e7d361e90 Mon Sep 17 00:00:00 2001 From: Queensly Acheampongmaa Date: Mon, 26 May 2025 23:09:24 +0000 Subject: [PATCH] Add PATCH support for Redfish DateTime fields in Manager resource This patch enables PATCH support for the DateTime and DateTimeLocalOffset fields of the Redfish Manager resource in sushy-tools, allowing clients like sushy to update the BMC clock through the Redfish interface. Key changes: - In managers.py, added set_datetime() and get_datetime() methods in the FakeDriver class. These store and retrieve datetime values as instance attributes (not indexed by UUID), since each instance represents a single manager. - In main.py, added PATCH support in the Manager resource handler. Removed reset_cache() hook to preserve instance state across requests, enabling PATCH updates to persist. - In test_main.py, mocked get_datetime() return values to support unit tests for the datetime PATCH logic. - In test_managers.py, added a unit test for the set_datetime() and get_datetime() methods. Also includes a release note describing the new functionality. Change-Id: I91c671316d8c646fa6cd80c926a2c8b872a7a1fa --- ...dfish-datatime-patch-dd2b43ef06bc957c.yaml | 9 ++ sushy_tools/emulator/main.py | 108 ++++++++++-------- sushy_tools/emulator/resources/managers.py | 32 +++++- .../unit/emulator/resources/test_managers.py | 24 ++++ sushy_tools/tests/unit/emulator/test_main.py | 3 + 5 files changed, 128 insertions(+), 48 deletions(-) create mode 100644 releasenotes/notes/add-redfish-datatime-patch-dd2b43ef06bc957c.yaml diff --git a/releasenotes/notes/add-redfish-datatime-patch-dd2b43ef06bc957c.yaml b/releasenotes/notes/add-redfish-datatime-patch-dd2b43ef06bc957c.yaml new file mode 100644 index 00000000..e0fe4c25 --- /dev/null +++ b/releasenotes/notes/add-redfish-datatime-patch-dd2b43ef06bc957c.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Adds PATCH support for the Redfish Manager resource's ``DateTime`` and + ``DateTimeLocalOffset`` fields in the dynamic emulator. Implements + ``get_datetime`` and ``set_datetime`` methods in the fake driver and + adds test support for mocking datetime values. Disables cache reset + to persist updates across requests. + diff --git a/sushy_tools/emulator/main.py b/sushy_tools/emulator/main.py index 2ee3a997..bfc51a37 100755 --- a/sushy_tools/emulator/main.py +++ b/sushy_tools/emulator/main.py @@ -85,10 +85,6 @@ class Application(flask.Flask): # This is needed for WSGI since it cannot process argv self.configure(config_file=os.environ.get('SUSHY_EMULATOR_CONFIG')) - @self.before_request - def reset_cache(): - self._cache = {} - def configure(self, config_file=None, extra_config=None): if config_file: self.config.from_pyfile(os.path.abspath(config_file)) @@ -346,59 +342,79 @@ def jsonify(obj_type, obj_version, obj): return flask.jsonify(obj) -@app.route('/redfish/v1/Managers/', methods=['GET']) +@app.route('/redfish/v1/Managers/', methods=['GET', 'PATCH']) @api_utils.returns_json def manager_resource(identity): if app.feature_set == "minimum": raise error.FeatureNotAvailable("Managers") - app.logger.debug('Serving resources for manager "%s"', identity) - manager = app.managers.get_manager(identity) systems = app.managers.get_managed_systems(manager) chassis = app.managers.get_managed_chassis(manager) uuid = manager['UUID'] - result = { - "Id": manager['Id'], - "Name": manager.get('Name'), - "UUID": uuid, - "ManagerType": "BMC", - "VirtualMedia": { - "@odata.id": "/redfish/v1/Systems/%s/VirtualMedia" % systems[0], - }, - "Links": { - "ManagerForServers": [ - { - "@odata.id": "/redfish/v1/Systems/%s" % system - } - for system in systems - ], - "ManagerForChassis": [ - { - "@odata.id": "/redfish/v1/Chassis/%s" % ch - } - for ch in chassis if app.feature_set == "full" - ] - }, - "@odata.id": "/redfish/v1/Managers/%s" % uuid, - } - if app.feature_set == "full": - result.update({ - "ServiceEntryPointUUID": manager.get('ServiceEntryPointUUID'), - "Description": "Contoso BMC", - "Model": "Joo Janta 200", - "DateTime": datetime.now().strftime('%Y-%M-%dT%H:%M:%S+00:00'), - "DateTimeLocalOffset": "+00:00", - "Status": { - "State": "Enabled", - "Health": "OK" - }, - "PowerState": "On", - "FirmwareVersion": "1.00", - }) - return jsonify('Manager', 'v1_3_1', result) + if flask.request.method == "GET": + app.logger.debug('Serving resources for manager "%s"', identity) + + result = { + "Id": manager['Id'], + "Name": manager.get('Name'), + "UUID": uuid, + "ManagerType": "BMC", + "VirtualMedia": { + "@odata.id": "/redfish/v1/Systems/%s/VirtualMedia" % systems[0] + }, + "Links": { + "ManagerForServers": [ + { + "@odata.id": "/redfish/v1/Systems/%s" % system + } + for system in systems + ], + "ManagerForChassis": [ + { + "@odata.id": "/redfish/v1/Chassis/%s" % ch + } + for ch in chassis if app.feature_set == "full" + ] + }, + "@odata.id": "/redfish/v1/Managers/%s" % uuid, + } + + if app.feature_set == "full": + dt_info = app.managers.get_datetime() + result.update({ + "ServiceEntryPointUUID": manager.get('ServiceEntryPointUUID'), + "Description": "Contoso BMC", + "Model": "Joo Janta 200", + "DateTime": dt_info.get( + "DateTime", + datetime.now().strftime('%Y-%m-%dT%H:%M:%S+00:00')), + "DateTimeLocalOffset": dt_info.get( + "DateTimeLocalOffset", "+00:00"), + "Status": { + "State": "Enabled", + "Health": "OK" + }, + "PowerState": "On", + "FirmwareVersion": "1.00", + }) + return jsonify('Manager', 'v1_3_1', result) + + elif flask.request.method == "PATCH": + if app.feature_set != "full": + raise error.MethodNotAllowed("PATCH not supported in minimum mode") + + data = flask.request.get_json(force=True) + new_datetime = data.get("DateTime") + new_offset = data.get("DateTimeLocalOffset") + + app.managers.set_datetime(new_datetime, new_offset) + + app.logger.debug("Updated DateTime for manager %s", identity) + + return '', 204 @app.route('/redfish/v1/Systems') diff --git a/sushy_tools/emulator/resources/managers.py b/sushy_tools/emulator/resources/managers.py index 4e5a8859..65edbf01 100644 --- a/sushy_tools/emulator/resources/managers.py +++ b/sushy_tools/emulator/resources/managers.py @@ -21,6 +21,8 @@ class FakeDriver(base.DriverBase): super().__init__(config, logger) self._systems = systems self._chassis = chassis + self._datetime = None + self._datetimelocaloffset = None def get_manager(self, identity): """Get a manager by its identity @@ -42,10 +44,36 @@ class FakeDriver(base.DriverBase): result = {'Id': system_uuid, 'UUID': system_uuid, 'Name': '%s-Manager' % system_name} - self._logger.debug('Found manager %(mgr)s by UUID %(id)s', - {'mgr': result, 'id': identity}) + + self._logger.debug( + 'Found manager %(mgr)s by UUID %(id)s', + {'mgr': result, 'id': identity} + ) return result + def set_datetime(self, datetime_value, datetimelocaloffset_value): + """Set the datetime and offset information for a manager + + :param datetime_value: The datetime string to set + :param datetimelocaloffset_value: + The time zone offset to set (e.g., "+00:00") + """ + self._datetime = datetime_value + self._datetimelocaloffset = datetimelocaloffset_value + + def get_datetime(self): + """Retrieve the datetime and offset information for a manager + + :returns: A dictionary with 'DateTime' and 'DateTimeLocalOffset' keys, + or an empty dict if no values are set + """ + if self._datetime and self._datetimelocaloffset: + return { + 'DateTime': self._datetime, + 'DateTimeLocalOffset': self._datetimelocaloffset + } + return {} + @property def driver(self): """Return human-friendly driver information diff --git a/sushy_tools/tests/unit/emulator/resources/test_managers.py b/sushy_tools/tests/unit/emulator/resources/test_managers.py index fd258e1c..65db372c 100644 --- a/sushy_tools/tests/unit/emulator/resources/test_managers.py +++ b/sushy_tools/tests/unit/emulator/resources/test_managers.py @@ -36,6 +36,9 @@ class FakeDriverTestCase(base.BaseTestCase): self.test_driver = managers.FakeDriver({}, mock.Mock(), self.systems, self.chassis) + self.datetime_value = "2025-06-11T12:00:00+00:00" + self.datetimelocaloffset_value = "+00:00" + def test_get_manager_not_found(self): self.systems.uuid.side_effect = error.FishyError('boom') self.assertRaises( @@ -52,3 +55,24 @@ class FakeDriverTestCase(base.BaseTestCase): def test_managed_systems(self): self.assertEqual( ['xxx'], self.test_driver.get_managed_systems(self.manager)) + + def test_set_datetime(self): + self.test_driver.set_datetime(self.datetime_value, + self.datetimelocaloffset_value) + self.assertEqual(self.test_driver._datetime, self.datetime_value) + self.assertEqual(self.test_driver._datetimelocaloffset, + self.datetimelocaloffset_value) + + def test_get_datetime_returns_expected_dict(self): + self.test_driver.set_datetime(self.datetime_value, + self.datetimelocaloffset_value) + result = self.test_driver.get_datetime() + expected = { + "DateTime": self.datetime_value, + "DateTimeLocalOffset": self.datetimelocaloffset_value + } + self.assertEqual(result, expected) + + def test_get_datetime_returns_empty_dict_when_not_set(self): + result = self.test_driver.get_datetime() + self.assertEqual(result, {}) diff --git a/sushy_tools/tests/unit/emulator/test_main.py b/sushy_tools/tests/unit/emulator/test_main.py index 8ef671a7..7d946384 100644 --- a/sushy_tools/tests/unit/emulator/test_main.py +++ b/sushy_tools/tests/unit/emulator/test_main.py @@ -221,6 +221,9 @@ class ManagersTestCase(EmulatorTestCase): } managers_mock.get_managed_systems.return_value = ['xxx'] managers_mock.get_managed_chassis.return_value = ['chassis0'] + managers_mock.get_datetime.return_value = { + "DateTime": "2025-06-02T12:00:00+00:00", + "DateTimeLocalOffset": "+00:00"} response = self.app.get('/redfish/v1/Managers/xxxx-yyyy-zzzz')