Add import, export configuration to idrac-redfish

Introduces import_configuration, export_configuration
and import_export_configuration steps.

Currently as MVP supports OEM section only.

Story: 2003594
Task: 40622

Depends-On: https://review.opendev.org/#/c/759425/

Change-Id: I10dbc30614d8654dda540ffaa5e61e7a9f03c7b3
This commit is contained in:
Aija Jauntēva 2020-07-16 04:38:41 -04:00
parent 851aac397e
commit 457d0cd70b
4 changed files with 949 additions and 8 deletions

View File

@ -34,7 +34,12 @@ opts = [
min=1,
help=_('Maximum number of retries for '
'the configuration job to complete '
'successfully.'))
'successfully.')),
cfg.IntOpt('query_import_config_job_status_interval',
min=0,
default=60,
help=_('Number of seconds to wait between checking for '
'completed import configuration task'))
]

View File

@ -20,8 +20,10 @@
DRAC management interface
"""
import json
import time
from futurist import periodics
from ironic_lib import metrics_utils
from oslo_log import log as logging
from oslo_utils import importutils
@ -29,15 +31,21 @@ from oslo_utils import importutils
from ironic.common import boot_devices
from ironic.common import exception
from ironic.common.i18n import _
from ironic.common import molds
from ironic.common import states
from ironic.conductor import task_manager
from ironic.conductor import utils as manager_utils
from ironic.conf import CONF
from ironic.drivers import base
from ironic.drivers.modules import deploy_utils
from ironic.drivers.modules.drac import common as drac_common
from ironic.drivers.modules.drac import job as drac_job
from ironic.drivers.modules.redfish import management as redfish_management
from ironic.drivers.modules.redfish import utils as redfish_utils
drac_exceptions = importutils.try_import('dracclient.exceptions')
sushy = importutils.try_import('sushy')
LOG = logging.getLogger(__name__)
@ -301,14 +309,333 @@ def set_boot_device(node, device, persistent=False):
class DracRedfishManagement(redfish_management.RedfishManagement):
"""iDRAC Redfish interface for management-related actions.
"""iDRAC Redfish interface for management-related actions."""
Presently, this class entirely defers to its base class, a generic,
vendor-independent Redfish interface. Future resolution of Dell EMC-
specific incompatibilities and introduction of vendor value added
should be implemented by this class.
"""
pass
EXPORT_CONFIGURATION_ARGSINFO = {
"export_configuration_location": {
"description": "URL of location to save the configuration to.",
"required": True,
}
}
IMPORT_CONFIGURATION_ARGSINFO = {
"import_configuration_location": {
"description": "URL of location to fetch desired configuration "
"from.",
"required": True,
}
}
IMPORT_EXPORT_CONFIGURATION_ARGSINFO = {**EXPORT_CONFIGURATION_ARGSINFO,
**IMPORT_CONFIGURATION_ARGSINFO}
@base.deploy_step(priority=0, argsinfo=EXPORT_CONFIGURATION_ARGSINFO)
@base.clean_step(priority=0, argsinfo=EXPORT_CONFIGURATION_ARGSINFO)
def export_configuration(self, task, export_configuration_location):
"""Export the configuration of the server.
Exports the configuration of the server against which the step is run
and stores it in specific format in indicated location.
Uses Dell's Server Configuration Profile (SCP) from `sushy-oem-idrac`
library to get ALL configuration for cloning.
:param task: A task from TaskManager.
:param export_configuration_location: URL of location to save the
configuration to.
:raises: MissingParameterValue if missing configuration name of a file
to save the configuration to
:raises: DracOperatationError when no managagers for Redfish system
found or configuration export from SCP failed
:raises: RedfishError when loading OEM extension failed
"""
if not export_configuration_location:
raise exception.MissingParameterValue(
_('export_configuration_location missing'))
system = redfish_utils.get_system(task.node)
configuration = None
if not system.managers:
raise exception.DracOperationError(
error=(_("No managers found for %(node)s"),
{'node': task.node.uuid}))
for manager in system.managers:
try:
manager_oem = manager.get_oem_extension('Dell')
except sushy.exceptions.OEMExtensionNotFoundError as e:
error_msg = (_("Search for Sushy OEM extension Python package "
"'sushy-oem-idrac' failed for node %(node)s. "
"Ensure it is installed. Error: %(error)s") %
{'node': task.node.uuid, 'error': e})
LOG.error(error_msg)
raise exception.RedfishError(error=error_msg)
try:
configuration = manager_oem.export_system_configuration()
LOG.info("Exported %(node)s configuration via OEM",
{'node': task.node.uuid})
except sushy.exceptions.SushyError as e:
LOG.debug("Sushy OEM extension Python package "
"'sushy-oem-idrac' failed to export system "
" configuration for node %(node)s. Will try next "
"manager, if available. Error: %(error)s",
{'system': system.uuid if system.uuid else
system.identity,
'manager': manager.uuid if manager.uuid else
manager.identity,
'node': task.node.uuid,
'error': e})
continue
break
if configuration and configuration.status_code == 200:
configuration = {"oem": {"interface": "idrac-redfish",
"data": configuration.json()}}
molds.save_configuration(task,
export_configuration_location,
configuration)
else:
raise exception.DracOperationError(
error=(_("No configuration exported for node %(node)s"),
{'node': task.node.uuid}))
@base.deploy_step(priority=0, argsinfo=IMPORT_CONFIGURATION_ARGSINFO)
@base.clean_step(priority=0, argsinfo=IMPORT_CONFIGURATION_ARGSINFO)
def import_configuration(self, task, import_configuration_location):
"""Import and apply the configuration to the server.
Gets pre-created configuration from storage by given location and
imports that into given server. Uses Dell's Server Configuration
Profile (SCP).
:param task: A task from TaskManager.
:param import_configuration_location: URL of location to fetch desired
configuration from.
:raises: MissingParameterValue if missing configuration name of a file
to fetch the configuration from
"""
if not import_configuration_location:
raise exception.MissingParameterValue(
_('import_configuration_location missing'))
configuration = molds.get_configuration(task,
import_configuration_location)
if not configuration:
raise exception.DracOperationError(
error=(_("No configuration found for node %(node)s by name "
"%(configuration_name)s"),
{'node': task.node.uuid,
'configuration_name': import_configuration_location}))
interface = configuration["oem"]["interface"]
if interface != "idrac-redfish":
raise exception.DracOperationError(
error=(_("Invalid configuration for node %(node)s "
"in %(configuration_name)s. Supports only "
"idrac-redfish, but found %(interface)s"),
{'node': task.node.uuid,
'configuration_name': import_configuration_location,
'interface': interface}))
system = redfish_utils.get_system(task.node)
if not system.managers:
raise exception.DracOperationError(
error=(_("No managers found for %(node)s"),
{'node': task.node.uuid}))
for manager in system.managers:
try:
manager_oem = manager.get_oem_extension('Dell')
except sushy.exceptions.OEMExtensionNotFoundError as e:
error_msg = (_("Search for Sushy OEM extension Python package "
"'sushy-oem-idrac' failed for node %(node)s. "
"Ensure it is installed. Error: %(error)s") %
{'node': task.node.uuid, 'error': e})
LOG.error(error_msg)
raise exception.RedfishError(error=error_msg)
try:
task_monitor = manager_oem.import_system_configuration(
json.dumps(configuration["oem"]["data"]))
info = task.node.driver_internal_info
info['import_task_monitor_url'] = task_monitor.task_monitor_uri
task.node.driver_internal_info = info
deploy_utils.set_async_step_flags(
task.node,
reboot=True,
skip_current_step=True,
polling=True)
deploy_opts = deploy_utils.build_agent_options(task.node)
task.driver.boot.prepare_ramdisk(task, deploy_opts)
manager_utils.node_power_action(task, states.REBOOT)
return deploy_utils.get_async_step_return_state(task.node)
except sushy.exceptions.SushyError as e:
LOG.debug("Sushy OEM extension Python package "
"'sushy-oem-idrac' failed to import system "
" configuration for node %(node)s. Will try next "
"manager, if available. Error: %(error)s",
{'system': system.uuid if system.uuid else
system.identity,
'manager': manager.uuid if manager.uuid else
manager.identity,
'node': task.node.uuid,
'error': e})
continue
raise exception.DracOperationError(
error=(_("Failed to import configuration for node %(node)s"),
{'node': task.node.uuid}))
@base.clean_step(priority=0,
argsinfo=IMPORT_EXPORT_CONFIGURATION_ARGSINFO)
@base.deploy_step(priority=0,
argsinfo=IMPORT_EXPORT_CONFIGURATION_ARGSINFO)
def import_export_configuration(self, task, import_configuration_location,
export_configuration_location):
"""Import and export configuration in one go.
Gets pre-created configuration from storage by given name and
imports that into given server. After that exports the configuration of
the server against which the step is run and stores it in specific
format in indicated storage as configured by Ironic.
:param import_configuration_location: URL of location to fetch desired
configuration from.
:param export_configuration_location: URL of location to save the
configuration to.
"""
# Import is async operation, setting sub-step to store export config
# and indicate that it's being executed as part of composite step
info = task.node.driver_internal_info
info['export_configuration_location'] = export_configuration_location
task.node.driver_internal_info = info
task.node.save()
return self.import_configuration(task, import_configuration_location)
# Export executed as part of Import async periodic task status check
@METRICS.timer('DracRedfishManagement._query_import_configuration_status')
@periodics.periodic(
spacing=CONF.drac.query_import_config_job_status_interval,
enabled=CONF.drac.query_import_config_job_status_interval > 0)
def _query_import_configuration_status(self, manager, context):
"""Period job to check import configuration task."""
filters = {'reserved': False, 'maintenance': False}
fields = ['driver_internal_info']
node_list = manager.iter_nodes(fields=fields, filters=filters)
for (node_uuid, driver, conductor_group,
driver_internal_info) in node_list:
try:
lock_purpose = 'checking async import configuration task'
with task_manager.acquire(context, node_uuid,
purpose=lock_purpose,
shared=True) as task:
if not isinstance(task.driver.management,
DracRedfishManagement):
continue
task_monitor_url = driver_internal_info.get(
'import_task_monitor_url')
if not task_monitor_url:
continue
self._check_import_configuration_task(
task, task_monitor_url)
except exception.NodeNotFound:
LOG.info('During _query_import_configuration_status, node '
'%(node)s was not found and presumed deleted by '
'another process.', {'node': node_uuid})
except exception.NodeLocked:
LOG.info('During _query_import_configuration_status, node '
'%(node)s was already locked by another process. '
'Skip.', {'node': node_uuid})
def _check_import_configuration_task(self, task, task_monitor_url):
"""Checks progress of running import configuration task"""
node = task.node
task_monitor = redfish_utils.get_task_monitor(node, task_monitor_url)
if not task_monitor.is_processing:
import_task = task_monitor.get_task()
task.upgrade_lock()
info = node.driver_internal_info
info.pop('import_task_monitor_url', None)
node.driver_internal_info = info
if (import_task.task_state == sushy.TASK_STATE_COMPLETED
and import_task.task_status in
[sushy.HEALTH_OK, sushy.HEALTH_WARNING]):
LOG.info('Configuration import %(task_monitor_url)s '
'successful for node %(node)s',
{'node': node.uuid,
'task_monitor_url': task_monitor_url})
# If import executed as part of import_export_configuration
export_configuration_location =\
info.get('export_configuration_location')
if export_configuration_location:
# then do sync export configuration before finishing
self._cleanup_export_substep(node)
try:
self.export_configuration(
task, export_configuration_location)
except (sushy.exceptions.SushyError,
exception.IronicException) as e:
error_msg = (_("Failed export configuration. %(exc)s" %
{'exc': e}))
log_msg = ("Export configuration failed for node "
"%(node)s. %(error)s" %
{'node': task.node.uuid,
'error': error_msg})
self._set_failed(task, log_msg, error_msg)
return
self._set_success(task)
else:
# Select all messages, skipping OEM messages that don't have
# `message` field populated.
messages = [m.message for m in import_task.messages
if m.message is not None]
error_msg = (_("Failed import configuration task: "
"%(task_monitor_url)s. Message: '%(message)s'.")
% {'task_monitor_url': task_monitor_url,
'message': ', '.join(messages)})
log_msg = ("Import configuration task failed for node "
"%(node)s. %(error)s" % {'node': task.node.uuid,
'error': error_msg})
self._set_failed(task, log_msg, error_msg)
node.save()
else:
LOG.debug('Import configuration %(task_monitor_url)s in progress '
'for node %(node)s',
{'node': node.uuid,
'task_monitor_url': task_monitor_url})
def _set_success(self, task):
if task.node.clean_step:
manager_utils.notify_conductor_resume_clean(task)
else:
manager_utils.notify_conductor_resume_deploy(task)
def _set_failed(self, task, log_msg, error_msg):
if task.node.clean_step:
manager_utils.cleaning_error_handler(task, log_msg, error_msg)
else:
manager_utils.deploying_error_handler(task, log_msg, error_msg)
def _cleanup_export_substep(self, node):
driver_internal_info = node.driver_internal_info
driver_internal_info.pop('export_configuration_location', None)
node.driver_internal_info = driver_internal_info
class DracWSManManagement(base.ManagementInterface):

View File

@ -20,20 +20,26 @@
Test class for DRAC management interface
"""
import json
from unittest import mock
from oslo_utils import importutils
import ironic.common.boot_devices
from ironic.common import exception
from ironic.common import molds
from ironic.conductor import task_manager
from ironic.conductor import utils as manager_utils
from ironic.drivers.modules import deploy_utils
from ironic.drivers.modules.drac import common as drac_common
from ironic.drivers.modules.drac import job as drac_job
from ironic.drivers.modules.drac import management as drac_mgmt
from ironic.drivers.modules.redfish import utils as redfish_utils
from ironic.tests.unit.drivers.modules.drac import utils as test_utils
from ironic.tests.unit.objects import utils as obj_utils
dracclient_exceptions = importutils.try_import('dracclient.exceptions')
sushy = importutils.try_import('sushy')
INFO_DICT = test_utils.INFO_DICT
@ -822,3 +828,598 @@ class DracManagementTestCase(test_utils.BaseDracTest):
job_ids=['JID_CLEARALL_FORCE'])
self.assertIsNone(return_value)
class DracRedfishManagementTestCase(test_utils.BaseDracTest):
def setUp(self):
super(DracRedfishManagementTestCase, self).setUp()
self.node = obj_utils.create_test_node(self.context,
driver='idrac',
driver_info=INFO_DICT)
self.management = drac_mgmt.DracRedfishManagement()
def test_export_configuration_name_missing(self):
task = mock.Mock(node=self.node, context=self.context)
self.assertRaises(exception.MissingParameterValue,
self.management.export_configuration, task, None)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_export_configuration_no_managers(self, mock_get_system):
task = mock.Mock(node=self.node, context=self.context)
fake_system = mock.Mock(managers=[])
mock_get_system.return_value = fake_system
self.assertRaises(exception.DracOperationError,
self.management.export_configuration, task, 'edge')
@mock.patch.object(drac_mgmt, 'LOG', autospec=True)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_export_configuration_oem_not_found(self, mock_get_system,
mock_log):
task = mock.Mock(node=self.node, context=self.context)
fake_manager1 = mock.Mock()
fake_manager1.get_oem_extension.side_effect = (
sushy.exceptions.OEMExtensionNotFoundError)
fake_system = mock.Mock(managers=[fake_manager1])
mock_get_system.return_value = fake_system
self.assertRaises(exception.RedfishError,
self.management.export_configuration, task, 'edge')
self.assertEqual(mock_log.error.call_count, 1)
@mock.patch.object(drac_mgmt, 'LOG', autospec=True)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_export_configuration_all_managers_fail(self, mock_get_system,
mock_log):
task = mock.Mock(node=self.node, context=self.context)
fake_manager_oem1 = mock.Mock()
fake_manager_oem1.export_system_configuration.side_effect = (
sushy.exceptions.SushyError)
fake_manager1 = mock.Mock()
fake_manager1.get_oem_extension.return_value = fake_manager_oem1
fake_manager_oem2 = mock.Mock()
fake_manager_oem2.export_system_configuration.side_effect = (
sushy.exceptions.SushyError)
fake_manager2 = mock.Mock()
fake_manager2.get_oem_extension.return_value = fake_manager_oem2
fake_system = mock.Mock(managers=[fake_manager1, fake_manager2])
mock_get_system.return_value = fake_system
self.assertRaises(exception.DracOperationError,
self.management.export_configuration,
task, 'edge')
self.assertEqual(mock_log.debug.call_count, 2)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_export_configuration_export_failed(self, mock_get_system):
task = mock.Mock(node=self.node, context=self.context)
fake_manager_oem1 = mock.Mock()
fake_manager_oem1.export_system_configuration = mock.Mock()
fake_manager_oem1.export_system_configuration.status_code = 500
fake_manager1 = mock.Mock()
fake_manager1.get_oem_extension.return_value = fake_manager_oem1
fake_system = mock.Mock(managers=[fake_manager1])
mock_get_system.return_value = fake_system
self.assertRaises(exception.DracOperationError,
self.management.export_configuration, task, 'edge')
@mock.patch.object(drac_mgmt, 'LOG', autospec=True)
@mock.patch.object(molds, 'save_configuration', autospec=True)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_export_configuration_success(self, mock_get_system,
mock_save_configuration,
mock_log):
task = mock.Mock(node=self.node, context=self.context)
fake_manager_oem1 = mock.Mock()
fake_manager_oem1.export_system_configuration.side_effect = (
sushy.exceptions.SushyError)
fake_manager1 = mock.Mock()
fake_manager1.get_oem_extension.return_value = fake_manager_oem1
configuration = mock.Mock(status_code=200)
configuration.json.return_value = (
json.loads('{"prop1":"value1", "prop2":2}'))
fake_manager_oem2 = mock.Mock()
fake_manager_oem2.export_system_configuration.return_value = (
configuration)
fake_manager2 = mock.Mock()
fake_manager2.get_oem_extension.return_value = fake_manager_oem2
fake_system = mock.Mock(managers=[fake_manager1, fake_manager2])
mock_get_system.return_value = fake_system
self.management.export_configuration(task, 'edge')
mock_save_configuration.assert_called_once_with(
task,
'edge',
{"oem": {"interface": "idrac-redfish",
"data": {"prop1": "value1", "prop2": 2}}})
self.assertEqual(mock_log.debug.call_count, 1)
self.assertEqual(mock_log.info.call_count, 1)
def test_import_configuration_name_missing(self):
task = mock.Mock(node=self.node, context=self.context)
self.assertRaises(exception.MissingParameterValue,
self.management.import_configuration, task, None)
@mock.patch.object(molds, 'get_configuration', autospec=True)
def test_import_configuration_file_not_found(self, mock_get_configuration):
task = mock.Mock(node=self.node, context=self.context)
mock_get_configuration.return_value = None
self.assertRaises(exception.DracOperationError,
self.management.import_configuration, task, 'edge')
@mock.patch.object(molds, 'get_configuration', autospec=True)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_import_configuration_no_managers(self, mock_get_system,
mock_get_configuration):
task = mock.Mock(node=self.node, context=self.context)
fake_system = mock.Mock(managers=[])
mock_get_configuration.return_value = json.loads(
'{"oem": {"interface": "idrac-redfish", '
'"data": {"prop1": "value1", "prop2": 2}}}')
mock_get_system.return_value = fake_system
self.assertRaises(exception.DracOperationError,
self.management.import_configuration, task, 'edge')
@mock.patch.object(drac_mgmt, 'LOG', autospec=True)
@mock.patch.object(molds, 'get_configuration', autospec=True)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_import_configuration_oem_not_found(self, mock_get_system,
mock_get_configuration,
mock_log):
task = mock.Mock(node=self.node, context=self.context)
fake_manager1 = mock.Mock()
fake_manager1.get_oem_extension.side_effect = (
sushy.exceptions.OEMExtensionNotFoundError)
fake_system = mock.Mock(managers=[fake_manager1])
mock_get_system.return_value = fake_system
mock_get_configuration.return_value = json.loads(
'{"oem": {"interface": "idrac-redfish", '
'"data": {"prop1": "value1", "prop2": 2}}}')
self.assertRaises(exception.RedfishError,
self.management.import_configuration, task, 'edge')
self.assertEqual(mock_log.error.call_count, 1)
@mock.patch.object(drac_mgmt, 'LOG', autospec=True)
@mock.patch.object(molds, 'get_configuration', autospec=True)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_import_configuration_all_managers_fail(self, mock_get_system,
mock_get_configuration,
mock_log):
task = mock.Mock(node=self.node, context=self.context)
fake_manager_oem1 = mock.Mock()
fake_manager_oem1.import_system_configuration.side_effect = (
sushy.exceptions.SushyError)
fake_manager1 = mock.Mock()
fake_manager1.get_oem_extension.return_value = fake_manager_oem1
fake_manager_oem2 = mock.Mock()
fake_manager_oem2.import_system_configuration.side_effect = (
sushy.exceptions.SushyError)
fake_manager2 = mock.Mock()
fake_manager2.get_oem_extension.return_value = fake_manager_oem2
fake_system = mock.Mock(managers=[fake_manager1, fake_manager2])
mock_get_system.return_value = fake_system
mock_get_configuration.return_value = json.loads(
'{"oem": {"interface": "idrac-redfish", '
'"data": {"prop1": "value1", "prop2": 2}}}')
self.assertRaises(exception.DracOperationError,
self.management.import_configuration, task, 'edge')
self.assertEqual(mock_log.debug.call_count, 2)
@mock.patch.object(molds, 'get_configuration', autospec=True)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_import_configuration_incorrect_interface(self, mock_get_system,
mock_get_configuration):
task = mock.Mock(node=self.node, context=self.context)
fake_manager_oem1 = mock.Mock()
fake_manager1 = mock.Mock()
fake_manager1.get_oem_extension.return_value = fake_manager_oem1
fake_system = mock.Mock(managers=[fake_manager1])
mock_get_system.return_value = fake_system
mock_get_configuration.return_value = json.loads(
'{"oem": {"interface": "idrac-wsman", '
'"data": {"prop1": "value1", "prop2": 2}}}')
self.assertRaises(exception.DracOperationError,
self.management.import_configuration, task, 'edge')
@mock.patch.object(deploy_utils, 'get_async_step_return_state',
autospec=True)
@mock.patch.object(deploy_utils, 'set_async_step_flags', autospec=True)
@mock.patch.object(deploy_utils, 'build_agent_options', autospec=True)
@mock.patch.object(manager_utils, 'node_power_action', autospec=True)
@mock.patch.object(drac_mgmt, 'LOG', autospec=True)
@mock.patch.object(molds, 'get_configuration', autospec=True)
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
def test_import_configuration_success(
self, mock_get_system, mock_get_configuration, mock_log,
mock_power, mock_build_agent_options,
mock_set_async_step_flags, mock_get_async_step_return_state):
deploy_opts = mock.Mock()
mock_build_agent_options.return_value = deploy_opts
step_result = mock.Mock()
mock_get_async_step_return_state.return_value = step_result
task = mock.Mock(node=self.node, context=self.context)
fake_manager_oem1 = mock.Mock()
fake_manager_oem1.import_system_configuration.side_effect = (
sushy.exceptions.SushyError)
fake_manager1 = mock.Mock()
fake_manager1.get_oem_extension.return_value = fake_manager_oem1
fake_manager_oem2 = mock.Mock()
fake_manager2 = mock.Mock()
fake_manager2.get_oem_extension.return_value = fake_manager_oem2
fake_system = mock.Mock(managers=[fake_manager1, fake_manager2])
mock_get_system.return_value = fake_system
mock_get_configuration.return_value = json.loads(
'{"oem": {"interface": "idrac-redfish", '
'"data": {"prop1": "value1", "prop2": 2}}}')
result = self.management.import_configuration(task, 'edge')
fake_manager_oem2.import_system_configuration.assert_called_once_with(
'{"prop1": "value1", "prop2": 2}')
self.assertEqual(mock_log.debug.call_count, 1)
mock_set_async_step_flags.assert_called_once_with(
task.node, reboot=True, skip_current_step=True, polling=True)
mock_build_agent_options.assert_called_once_with(task.node)
task.driver.boot.prepare_ramdisk.assert_called_once_with(
task, deploy_opts)
mock_get_async_step_return_state.assert_called_once_with(task.node)
self.assertEqual(step_result, result)
@mock.patch.object(drac_mgmt.DracRedfishManagement,
'import_configuration', autospec=True)
def test_import_export_configuration_success(self, mock_import):
task = mock.Mock(node=self.node, context=self.context)
self.management.import_export_configuration(
task, 'https://server/edge_import', 'https://server/edge_export')
mock_import.assert_called_once_with(self.management, task,
'https://server/edge_import')
self.assertEqual(
'https://server/edge_export',
self.node.driver_internal_info.get(
'export_configuration_location'))
@mock.patch.object(task_manager, 'acquire', autospec=True)
def test__query_import_configuration_not_drac(self, mock_acquire):
driver_internal_info = {'import_task_monitor_url': '/TaskService/123'}
self.node.driver_internal_info = driver_internal_info
self.node.save()
mock_manager = mock.Mock()
node_list = [(self.node.uuid, 'not-idrac', '', driver_internal_info)]
mock_manager.iter_nodes.return_value = node_list
task = mock.Mock(node=self.node,
driver=mock.Mock(management=mock.Mock()))
mock_acquire.return_value = mock.MagicMock(
__enter__=mock.MagicMock(return_value=task))
self.management._check_import_configuration_task = mock.Mock()
self.management._query_import_configuration_status(mock_manager,
self.context)
self.management._check_import_configuration_task.assert_not_called()
@mock.patch.object(task_manager, 'acquire', autospec=True)
def test__query_import_configuration_status_no_task_monitor_url(
self, mock_acquire):
driver_internal_info = {'something': 'else'}
self.node.driver_internal_info = driver_internal_info
self.node.save()
mock_manager = mock.Mock()
node_list = [(self.node.uuid, 'idrac', '', driver_internal_info)]
mock_manager.iter_nodes.return_value = node_list
task = mock.Mock(node=self.node,
driver=mock.Mock(management=self.management))
mock_acquire.return_value = mock.MagicMock(
__enter__=mock.MagicMock(return_value=task))
self.management._check_import_configuration_task = mock.Mock()
self.management._query_import_configuration_status(mock_manager,
self.context)
self.management._check_import_configuration_task.assert_not_called()
@mock.patch.object(drac_mgmt.LOG, 'info', autospec=True)
@mock.patch.object(task_manager, 'acquire', autospec=True)
def test__query_import_configuration_status_node_notfound(
self, mock_acquire, mock_log):
driver_internal_info = {'import_task_monitor_url': '/TaskService/123'}
self.node.driver_internal_info = driver_internal_info
self.node.save()
mock_manager = mock.Mock()
node_list = [(self.node.uuid, 'idrac', '', driver_internal_info)]
mock_manager.iter_nodes.return_value = node_list
mock_acquire.side_effect = exception.NodeNotFound
task = mock.Mock(node=self.node,
driver=mock.Mock(management=self.management))
mock_acquire.return_value = mock.MagicMock(
__enter__=mock.MagicMock(return_value=task))
self.management._check_import_configuration_task = mock.Mock()
self.management._query_import_configuration_status(mock_manager,
self.context)
self.management._check_import_configuration_task.assert_not_called()
self.assertTrue(mock_log.called)
@mock.patch.object(drac_mgmt.LOG, 'info', autospec=True)
@mock.patch.object(task_manager, 'acquire', autospec=True)
def test__query_import_configuration_status_node_locked(
self, mock_acquire, mock_log):
driver_internal_info = {'import_task_monitor_url': '/TaskService/123'}
self.node.driver_internal_info = driver_internal_info
self.node.save()
mock_manager = mock.Mock()
node_list = [(self.node.uuid, 'idrac', '', driver_internal_info)]
mock_manager.iter_nodes.return_value = node_list
mock_acquire.side_effect = exception.NodeLocked
task = mock.Mock(node=self.node,
driver=mock.Mock(management=self.management))
mock_acquire.return_value = mock.MagicMock(
__enter__=mock.MagicMock(return_value=task))
self.management._check_import_configuration_task = mock.Mock()
self.management._query_import_configuration_status(mock_manager,
self.context)
self.management._check_import_configuration_task.assert_not_called()
self.assertTrue(mock_log.called)
@mock.patch.object(task_manager, 'acquire', autospec=True)
def test__query_import_configuration_status(self, mock_acquire):
driver_internal_info = {'import_task_monitor_url': '/TaskService/123'}
self.node.driver_internal_info = driver_internal_info
self.node.save()
mock_manager = mock.Mock()
node_list = [(self.node.uuid, 'idrac', '', driver_internal_info)]
mock_manager.iter_nodes.return_value = node_list
task = mock.Mock(node=self.node,
driver=mock.Mock(management=self.management))
mock_acquire.return_value = mock.MagicMock(
__enter__=mock.MagicMock(return_value=task))
self.management._check_import_configuration_task = mock.Mock()
self.management._query_import_configuration_status(mock_manager,
self.context)
(self.management
._check_import_configuration_task
.assert_called_once_with(task, '/TaskService/123'))
@mock.patch.object(drac_mgmt.LOG, 'debug', autospec=True)
@mock.patch.object(redfish_utils, 'get_task_monitor', autospec=True)
def test__check_import_configuration_task_still_processing(
self, mock_get_task_monitor, mock_log):
driver_internal_info = {'import_task_monitor_url': '/TaskService/123'}
self.node.driver_internal_info = driver_internal_info
self.node.save()
mock_task_monitor = mock.Mock()
mock_task_monitor.is_processing = True
mock_get_task_monitor.return_value = mock_task_monitor
self.management._set_success = mock.Mock()
self.management._set_failed = mock.Mock()
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.management._check_import_configuration_task(
task, '/TaskService/123')
self.management._set_success.assert_not_called()
self.management._set_failed.assert_not_called()
self.assertTrue(mock_log.called)
self.assertEqual(
'/TaskService/123',
task.node.driver_internal_info.get('import_task_monitor_url'))
@mock.patch.object(redfish_utils, 'get_task_monitor', autospec=True)
def test__check_import_configuration_task_failed(
self, mock_get_task_monitor):
driver_internal_info = {'import_task_monitor_url': '/TaskService/123'}
self.node.driver_internal_info = driver_internal_info
self.node.save()
mock_message = mock.Mock()
mock_message.message = 'Firmware upgrade failed'
mock_import_task = mock.Mock()
mock_import_task.task_state = sushy.TASK_STATE_COMPLETED
mock_import_task.task_status = 'Failed'
mock_import_task.messages = [mock_message]
mock_task_monitor = mock.Mock()
mock_task_monitor.is_processing = False
mock_task_monitor.get_task.return_value = mock_import_task
mock_get_task_monitor.return_value = mock_task_monitor
self.management._set_success = mock.Mock()
self.management._set_failed = mock.Mock()
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.management._check_import_configuration_task(
task, '/TaskService/123')
self.management._set_failed.assert_called_once_with(
task, mock.ANY,
"Failed import configuration task: /TaskService/123. Message: "
"'Firmware upgrade failed'.")
self.management._set_success.assert_not_called()
self.assertIsNone(
task.node.driver_internal_info.get('import_task_monitor_url'))
@mock.patch.object(redfish_utils, 'get_task_monitor', autospec=True)
def test__check_import_configuration_task(self, mock_get_task_monitor):
driver_internal_info = {'import_task_monitor_url': '/TaskService/123'}
self.node.driver_internal_info = driver_internal_info
self.node.save()
mock_message = mock.Mock()
mock_message.message = 'Configuration import done'
mock_import_task = mock.Mock()
mock_import_task.task_state = sushy.TASK_STATE_COMPLETED
mock_import_task.task_status = sushy.HEALTH_OK
mock_import_task.messages = [mock_message]
mock_task_monitor = mock.Mock()
mock_task_monitor.is_processing = False
mock_task_monitor.get_task.return_value = mock_import_task
mock_get_task_monitor.return_value = mock_task_monitor
self.management._set_success = mock.Mock()
self.management._set_failed = mock.Mock()
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.management._check_import_configuration_task(
task, '/TaskService/123')
self.management._set_success.assert_called_once_with(task)
self.management._set_failed.assert_not_called()
self.assertIsNone(
task.node.driver_internal_info.get('import_task_monitor_url'))
@mock.patch.object(redfish_utils, 'get_task_monitor', autospec=True)
def test__check_import_configuration_task_with_export_failed(
self, mock_get_task_monitor):
driver_internal_info = {
'import_task_monitor_url': '/TaskService/123',
'export_configuration_location': 'https://server/export1'}
self.node.driver_internal_info = driver_internal_info
self.node.save()
mock_message = mock.Mock()
mock_message.message = 'Configuration import done'
mock_import_task = mock.Mock()
mock_import_task.task_state = sushy.TASK_STATE_COMPLETED
mock_import_task.task_status = sushy.HEALTH_OK
mock_import_task.messages = [mock_message]
mock_task_monitor = mock.Mock()
mock_task_monitor.is_processing = False
mock_task_monitor.get_task.return_value = mock_import_task
mock_get_task_monitor.return_value = mock_task_monitor
self.management._set_success = mock.Mock()
self.management._set_failed = mock.Mock()
mock_export = mock.Mock()
mock_export.side_effect = exception.IronicException
self.management.export_configuration = mock_export
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.management._check_import_configuration_task(
task, '/TaskService/123')
self.management.export_configuration.assert_called_once_with(
task, 'https://server/export1')
self.management._set_success.assert_not_called()
self.assertIsNone(
task.node.driver_internal_info.get('import_task_monitor_url'))
self.assertIsNone(
task.node.driver_internal_info.get(
'export_configuration_location'))
self.management._set_failed.assert_called_with(
task, mock.ANY,
'Failed export configuration. An unknown exception occurred.')
@mock.patch.object(redfish_utils, 'get_task_monitor', autospec=True)
def test__check_import_configuration_task_with_export(
self, mock_get_task_monitor):
driver_internal_info = {
'import_task_monitor_url': '/TaskService/123',
'export_configuration_location': 'https://server/export1'}
self.node.driver_internal_info = driver_internal_info
self.node.save()
mock_message = mock.Mock()
mock_message.message = 'Configuration import done'
mock_import_task = mock.Mock()
mock_import_task.task_state = sushy.TASK_STATE_COMPLETED
mock_import_task.task_status = sushy.HEALTH_OK
mock_import_task.messages = [mock_message]
mock_task_monitor = mock.Mock()
mock_task_monitor.is_processing = False
mock_task_monitor.get_task.return_value = mock_import_task
mock_get_task_monitor.return_value = mock_task_monitor
self.management._set_success = mock.Mock()
self.management._set_failed = mock.Mock()
self.management.export_configuration = mock.Mock()
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.management._check_import_configuration_task(
task, '/TaskService/123')
self.management.export_configuration.assert_called_once_with(
task, 'https://server/export1')
self.management._set_success.assert_called_once_with(task)
self.assertIsNone(
task.node.driver_internal_info.get('import_task_monitor_url'))
self.assertIsNone(
task.node.driver_internal_info.get(
'export_configuration_location'))
self.management._set_failed.assert_not_called()
@mock.patch.object(manager_utils, 'notify_conductor_resume_deploy',
autospec=True)
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean',
autospec=True)
def test__set_success_clean(self, mock_notify_clean, mock_notify_deploy):
self.node.clean_step = {'test': 'value'}
self.node.save()
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.management._set_success(task)
mock_notify_clean.assert_called_once_with(task)
@mock.patch.object(manager_utils, 'notify_conductor_resume_deploy',
autospec=True)
@mock.patch.object(manager_utils, 'notify_conductor_resume_clean',
autospec=True)
def test__set_success_deploy(self, mock_notify_clean, mock_notify_deploy):
self.node.clean_step = None
self.node.save()
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.management._set_success(task)
mock_notify_deploy.assert_called_once_with(task)
@mock.patch.object(manager_utils, 'deploying_error_handler',
autospec=True)
@mock.patch.object(manager_utils, 'cleaning_error_handler',
autospec=True)
def test__set_failed_clean(self, mock_clean_handler, mock_deploy_handler):
self.node.clean_step = {'test': 'value'}
self.node.save()
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.management._set_failed(task, 'error', 'log message')
mock_clean_handler.assert_called_once_with(
task, 'error', 'log message')
@mock.patch.object(manager_utils, 'deploying_error_handler',
autospec=True)
@mock.patch.object(manager_utils, 'cleaning_error_handler',
autospec=True)
def test__set_failed_deploy(self, mock_clean_handler, mock_deploy_handler):
self.node.clean_step = None
self.node.save()
with task_manager.acquire(self.context, self.node.uuid,
shared=False) as task:
self.management._set_failed(task, 'error', 'log message')
mock_deploy_handler.assert_called_once_with(
task, 'error', 'log message')

View File

@ -0,0 +1,8 @@
---
features:
- |
Adds ``import_configuration``, ``export_configuration`` and
``import_export_configuration`` steps to ``idrac-redfish`` management
interface. These steps allow to use configuration from another system as
template and replicate that configuration to other, similarly capable,
systems. Currently, this feature is experimental.