diff --git a/ironic/conf/drac.py b/ironic/conf/drac.py index 0b3b2bdc66..ebf402ce17 100644 --- a/ironic/conf/drac.py +++ b/ironic/conf/drac.py @@ -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')) ] diff --git a/ironic/drivers/modules/drac/management.py b/ironic/drivers/modules/drac/management.py index f595bea3be..e069e086c4 100644 --- a/ironic/drivers/modules/drac/management.py +++ b/ironic/drivers/modules/drac/management.py @@ -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): diff --git a/ironic/tests/unit/drivers/modules/drac/test_management.py b/ironic/tests/unit/drivers/modules/drac/test_management.py index 27de5f7d53..227d5cd5f7 100644 --- a/ironic/tests/unit/drivers/modules/drac/test_management.py +++ b/ironic/tests/unit/drivers/modules/drac/test_management.py @@ -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') diff --git a/releasenotes/notes/add-config-mold-steps-idrac-1773d81953209964.yaml b/releasenotes/notes/add-config-mold-steps-idrac-1773d81953209964.yaml new file mode 100644 index 0000000000..00295acefa --- /dev/null +++ b/releasenotes/notes/add-config-mold-steps-idrac-1773d81953209964.yaml @@ -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.