From 1020e805d368684080660f10a5ee207d119c2b07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aija=20Jaunt=C4=93va?= Date: Wed, 13 Jan 2021 08:13:33 -0500 Subject: [PATCH] Add import system configuration method set_virtual_boot_device is also using the import system configuration action, but its usage is very specific, with retries and rebooting. To keep it simple, this adds import_system_configuration, which is asynchronous and returns a TaskMonitor. Additionally, this changes the case of the header field names used by the asynchronous.http_call method to make them work with unit tests. While the requests package can handle real header field names case- insensitively, when they are mocked in unit tests, case needs to match. Story: 2003594 Task: 41576 Change-Id: I3b5e620e7b1939a029bd59b4578c0f52c8789598 --- requirements.txt | 2 +- sushy_oem_idrac/asynchronous.py | 4 +- .../resources/manager/constants.py | 21 ++++++++ sushy_oem_idrac/resources/manager/manager.py | 54 ++++++++++++++++++- sushy_oem_idrac/resources/manager/mappings.py | 9 ++++ sushy_oem_idrac/tests/unit/test_manager.py | 47 +++++++++++++++- 6 files changed, 132 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index a85b46e..d09b88e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,4 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0 python-dateutil>=2.7.0 # BSD -sushy>=2.0.0 # Apache-2.0 +sushy>=3.7.0 # Apache-2.0 diff --git a/sushy_oem_idrac/asynchronous.py b/sushy_oem_idrac/asynchronous.py index 881860d..d959ffd 100644 --- a/sushy_oem_idrac/asynchronous.py +++ b/sushy_oem_idrac/asynchronous.py @@ -44,7 +44,7 @@ def http_call(conn, method, *args, **kwargs): location = None while response.status_code == 202: - location = response.headers.get('location', location) + location = response.headers.get('Location', location) if not location: raise sushy.exceptions.ExtensionError( error='Response %d to HTTP %s with args %s, kwargs %s ' @@ -52,7 +52,7 @@ def http_call(conn, method, *args, **kwargs): 'header' % (response.status_code, method.upper(), args, kwargs)) - retry_after = response.headers.get('retry-after') + retry_after = response.headers.get('Retry-After') if retry_after: retry_after = _to_datetime(retry_after) sleep_for = max(0, (retry_after - datetime.now()).total_seconds()) diff --git a/sushy_oem_idrac/resources/manager/constants.py b/sushy_oem_idrac/resources/manager/constants.py index 0e3cf9f..e94a4b3 100644 --- a/sushy_oem_idrac/resources/manager/constants.py +++ b/sushy_oem_idrac/resources/manager/constants.py @@ -37,3 +37,24 @@ RESET_IDRAC_GRACEFUL_RESTART = 'graceful restart' RESET_IDRAC_FORCE_RESTART = 'force restart' """Perform an immediate (non-graceful) shutdown, followed by a restart""" + +# ImportSystemConfiguration ShutdownType values +IMPORT_SHUTDOWN_GRACEFUL = 'graceful shutdown' +"""Graceful shutdown for Import System Configuration + +Will wait for the host up to 5 minutes to shut down before timing out. The +operating system can potentially deny or ignore the graceful shutdown request. +""" + +IMPORT_SHUTDOWN_FORCED = 'forced shutdown' +"""Forced shutdown for Import System Configuration + +The host server will be powered off immediately. Should be used when it is safe +to power down the host. +""" + +IMPORT_SHUTDOWN_NO_REBOOT = 'no shutdown' +"""No reboot for Import System Configuration + +No shutdown performed. Explicit reboot is necessary to apply changes. +""" diff --git a/sushy_oem_idrac/resources/manager/manager.py b/sushy_oem_idrac/resources/manager/manager.py index 7a01dc8..5257806 100644 --- a/sushy_oem_idrac/resources/manager/manager.py +++ b/sushy_oem_idrac/resources/manager/manager.py @@ -19,6 +19,7 @@ import sushy from sushy.resources import base from sushy.resources import common from sushy.resources.oem import base as oem_base +from sushy.taskmonitor import TaskMonitor from sushy import utils as sushy_utils from sushy_oem_idrac import asynchronous @@ -48,8 +49,13 @@ class ExportActionField(common.ActionField): shared_parameters = SharedParameters('ShareParameters') +class ImportActionField(common.ActionField): + allowed_shutdown_type_values = base.Field( + 'ShutdownType@Redfish.AllowableValues', adapter=list) + + class DellManagerActionsField(base.CompositeField): - import_system_configuration = common.ActionField( + import_system_configuration = ImportActionField( lambda key, **kwargs: key.endswith( '#OemManager.ImportSystemConfiguration')) @@ -350,6 +356,52 @@ VFDD\ LOG.error(error) raise sushy.exceptions.ExtensionError(error=error) + def get_allowed_import_shutdown_type_values(self): + """Get the allowed shutdown types of import system configuration. + + :returns: A set of allowed shutdown type values. + """ + import_action = self._actions.import_system_configuration + allowed_values = import_action.allowed_shutdown_type_values + + if not allowed_values: + LOG.warning('Could not figure out the allowed values for the ' + 'shutdown type of import system configuration at %s', + self.path) + return set(mgr_maps.IMPORT_SHUTDOWN_VALUE_MAP_REV) + + return set([mgr_maps.IMPORT_SHUTDOWN_VALUE_MAP[value] for value in + set(mgr_maps.IMPORT_SHUTDOWN_VALUE_MAP). + intersection(allowed_values)]) + + def import_system_configuration(self, import_buffer): + """Imports system configuration. + + Caller needs to handle system reboot separately. + + :param import_buffer: Configuration data to be imported. + :returns: Task monitor instance to watch for task completion + """ + action_data = dict(self.ACTION_DATA, ImportBuffer=import_buffer) + # Caller needs to handle system reboot separately to preserve + # one-time boot settings. + shutdown_type = mgr_cons.IMPORT_SHUTDOWN_NO_REBOOT + + allowed_shutdown_types = self.get_allowed_import_shutdown_type_values() + if shutdown_type not in allowed_shutdown_types: + raise sushy.exceptions.InvalidParameterValueError( + parameter='shutdown_type', value=shutdown_type, + valid_values=allowed_shutdown_types) + + action_data['ShutdownType'] =\ + mgr_maps.IMPORT_SHUTDOWN_VALUE_MAP_REV[shutdown_type] + + response = self._conn.post(self.import_system_configuration_uri, + data=action_data) + + return TaskMonitor.from_response( + self._conn, response, self.import_system_configuration_uri) + def get_extension(*args, **kwargs): return DellManagerExtension diff --git a/sushy_oem_idrac/resources/manager/mappings.py b/sushy_oem_idrac/resources/manager/mappings.py index c111457..1eb8072 100644 --- a/sushy_oem_idrac/resources/manager/mappings.py +++ b/sushy_oem_idrac/resources/manager/mappings.py @@ -32,3 +32,12 @@ RESET_IDRAC_VALUE_MAP = { } RESET_IDRAC_VALUE_MAP_REV = utils.revert_dictionary(RESET_IDRAC_VALUE_MAP) + +IMPORT_SHUTDOWN_VALUE_MAP = { + 'Graceful': mgr_cons.IMPORT_SHUTDOWN_GRACEFUL, + 'Forced': mgr_cons.IMPORT_SHUTDOWN_FORCED, + 'NoReboot': mgr_cons.IMPORT_SHUTDOWN_NO_REBOOT +} + +IMPORT_SHUTDOWN_VALUE_MAP_REV =\ + utils.revert_dictionary(IMPORT_SHUTDOWN_VALUE_MAP) diff --git a/sushy_oem_idrac/tests/unit/test_manager.py b/sushy_oem_idrac/tests/unit/test_manager.py index baf861c..8ddfa4a 100644 --- a/sushy_oem_idrac/tests/unit/test_manager.py +++ b/sushy_oem_idrac/tests/unit/test_manager.py @@ -20,12 +20,14 @@ from unittest import mock from oslotest.base import BaseTestCase import sushy from sushy.resources.manager import manager +from sushy.taskmonitor import TaskMonitor from sushy_oem_idrac.resources.manager import constants as mgr_cons from sushy_oem_idrac.resources.manager import idrac_card_service as idrac_card from sushy_oem_idrac.resources.manager import job_collection as jc from sushy_oem_idrac.resources.manager import job_service as job from sushy_oem_idrac.resources.manager import lifecycle_service as lifecycle +from sushy_oem_idrac.resources.manager import manager as oem_manager class ManagerTestCase(BaseTestCase): @@ -41,7 +43,9 @@ class ManagerTestCase(BaseTestCase): mock_response = self.conn.post.return_value mock_response.status_code = 202 - mock_response.headers.get.return_value = '1' + mock_response.headers = { + 'Location': '/redfish/v1/TaskService/Tasks/JID_905749031119'} + mock_response.content = None self.manager = manager.Manager(self.conn, '/redfish/v1/Managers/BMC', redfish_version='1.0.2') @@ -215,3 +219,44 @@ class ManagerTestCase(BaseTestCase): job_collection.path) self.assertIsInstance(job_collection, jc.DellJobCollection) + + def test_get_allowed_import_shutdown_type_values(self): + oem = self.manager.get_oem_extension('Dell') + expected_values = {mgr_cons.IMPORT_SHUTDOWN_GRACEFUL, + mgr_cons.IMPORT_SHUTDOWN_FORCED, + mgr_cons.IMPORT_SHUTDOWN_NO_REBOOT} + allowed_values = oem.get_allowed_import_shutdown_type_values() + self.assertIsInstance(allowed_values, set) + self.assertEqual(expected_values, allowed_values) + + @mock.patch.object(oem_manager, 'LOG', autospec=True) + def test_get_allowed_import_shutdown_type_values_missing(self, mock_log): + oem = self.manager.get_oem_extension('Dell') + import_action = ('OemManager.v1_0_0' + '#OemManager.ImportSystemConfiguration') + oem.json['Actions']['Oem'][import_action].pop( + 'ShutdownType@Redfish.AllowableValues') + oem.refresh() + expected_values = {mgr_cons.IMPORT_SHUTDOWN_GRACEFUL, + mgr_cons.IMPORT_SHUTDOWN_FORCED, + mgr_cons.IMPORT_SHUTDOWN_NO_REBOOT} + allowed_values = oem.get_allowed_import_shutdown_type_values() + self.assertIsInstance(allowed_values, set) + self.assertEqual(expected_values, allowed_values) + mock_log.warning.assert_called_once() + + def test_import_system_configuration(self): + oem = self.manager.get_oem_extension('Dell') + + result = oem.import_system_configuration('{"key": "value"}') + + self.conn.post.assert_called_once_with( + '/redfish/v1/Managers/iDRAC.Embedded.1/Actions/Oem/EID_674_Manager' + '.ImportSystemConfiguration', data={'ShareParameters': + {'Target': 'ALL'}, + 'ImportBuffer': + '{"key": "value"}', + 'ShutdownType': 'NoReboot'}) + self.assertIsInstance(result, TaskMonitor) + self.assertEqual('/redfish/v1/TaskService/Tasks/JID_905749031119', + result.task_monitor_uri)