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:
parent
ed70d136ac
commit
1020e805d3
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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.
|
||||||
|
"""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue