summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2021-03-31 11:53:03 +0000
committerGerrit Code Review <review@openstack.org>2021-03-31 11:53:03 +0000
commitff4c370d4b38f67b3b0e47e1c19085b8d4beb461 (patch)
tree40e5e686b710a1fe88e0349e2e99adf0033a4dea
parent4e8a3fdc29afc8bca10d03336c7d648d1afc97aa (diff)
parent457d0cd70bdb02b37cb8cd87c13d51bee1a7e8cb (diff)
downloadironic-ff4c370d4b38f67b3b0e47e1c19085b8d4beb461.tar.gz
Merge "Add import, export configuration to idrac-redfish"
-rw-r--r--ironic/conf/drac.py7
-rw-r--r--ironic/drivers/modules/drac/management.py341
-rw-r--r--ironic/tests/unit/drivers/modules/drac/test_management.py601
-rw-r--r--releasenotes/notes/add-config-mold-steps-idrac-1773d81953209964.yaml8
4 files changed, 949 insertions, 8 deletions
diff --git a/ironic/conf/drac.py b/ironic/conf/drac.py
index 0b3b2bdc6..ebf402ce1 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 f595bea3b..e069e086c 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."""
+
+ 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
- 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
+ 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 27de5f7d5..227d5cd5f 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 000000000..00295acef
--- /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.