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
This commit is contained in:
Aija Jauntēva 2021-01-13 08:13:33 -05:00
parent ed70d136ac
commit 1020e805d3
6 changed files with 132 additions and 5 deletions

View File

@ -4,4 +4,4 @@
pbr!=2.1.0,>=2.0.0 # Apache-2.0 pbr!=2.1.0,>=2.0.0 # Apache-2.0
python-dateutil>=2.7.0 # BSD python-dateutil>=2.7.0 # BSD
sushy>=2.0.0 # Apache-2.0 sushy>=3.7.0 # Apache-2.0

View File

@ -44,7 +44,7 @@ def http_call(conn, method, *args, **kwargs):
location = None location = None
while response.status_code == 202: while response.status_code == 202:
location = response.headers.get('location', location) location = response.headers.get('Location', location)
if not location: if not location:
raise sushy.exceptions.ExtensionError( raise sushy.exceptions.ExtensionError(
error='Response %d to HTTP %s with args %s, kwargs %s ' 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(), 'header' % (response.status_code, method.upper(),
args, kwargs)) args, kwargs))
retry_after = response.headers.get('retry-after') retry_after = response.headers.get('Retry-After')
if retry_after: if retry_after:
retry_after = _to_datetime(retry_after) retry_after = _to_datetime(retry_after)
sleep_for = max(0, (retry_after - datetime.now()).total_seconds()) sleep_for = max(0, (retry_after - datetime.now()).total_seconds())

View File

@ -37,3 +37,24 @@ RESET_IDRAC_GRACEFUL_RESTART = 'graceful restart'
RESET_IDRAC_FORCE_RESTART = 'force restart' RESET_IDRAC_FORCE_RESTART = 'force restart'
"""Perform an immediate (non-graceful) shutdown, followed by a 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.
"""

View File

@ -19,6 +19,7 @@ import sushy
from sushy.resources import base from sushy.resources import base
from sushy.resources import common from sushy.resources import common
from sushy.resources.oem import base as oem_base from sushy.resources.oem import base as oem_base
from sushy.taskmonitor import TaskMonitor
from sushy import utils as sushy_utils from sushy import utils as sushy_utils
from sushy_oem_idrac import asynchronous from sushy_oem_idrac import asynchronous
@ -48,8 +49,13 @@ class ExportActionField(common.ActionField):
shared_parameters = SharedParameters('ShareParameters') shared_parameters = SharedParameters('ShareParameters')
class ImportActionField(common.ActionField):
allowed_shutdown_type_values = base.Field(
'ShutdownType@Redfish.AllowableValues', adapter=list)
class DellManagerActionsField(base.CompositeField): class DellManagerActionsField(base.CompositeField):
import_system_configuration = common.ActionField( import_system_configuration = ImportActionField(
lambda key, **kwargs: key.endswith( lambda key, **kwargs: key.endswith(
'#OemManager.ImportSystemConfiguration')) '#OemManager.ImportSystemConfiguration'))
@ -350,6 +356,52 @@ VFDD\
LOG.error(error) LOG.error(error)
raise sushy.exceptions.ExtensionError(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): def get_extension(*args, **kwargs):
return DellManagerExtension return DellManagerExtension

View File

@ -32,3 +32,12 @@ RESET_IDRAC_VALUE_MAP = {
} }
RESET_IDRAC_VALUE_MAP_REV = utils.revert_dictionary(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)

View File

@ -20,12 +20,14 @@ from unittest import mock
from oslotest.base import BaseTestCase from oslotest.base import BaseTestCase
import sushy import sushy
from sushy.resources.manager import manager 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 constants as mgr_cons
from sushy_oem_idrac.resources.manager import idrac_card_service as idrac_card 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_collection as jc
from sushy_oem_idrac.resources.manager import job_service as job 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 lifecycle_service as lifecycle
from sushy_oem_idrac.resources.manager import manager as oem_manager
class ManagerTestCase(BaseTestCase): class ManagerTestCase(BaseTestCase):
@ -41,7 +43,9 @@ class ManagerTestCase(BaseTestCase):
mock_response = self.conn.post.return_value mock_response = self.conn.post.return_value
mock_response.status_code = 202 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', self.manager = manager.Manager(self.conn, '/redfish/v1/Managers/BMC',
redfish_version='1.0.2') redfish_version='1.0.2')
@ -215,3 +219,44 @@ class ManagerTestCase(BaseTestCase):
job_collection.path) job_collection.path)
self.assertIsInstance(job_collection, self.assertIsInstance(job_collection,
jc.DellJobCollection) 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)