From 32c9c7445978650093e2d3b2413b9e97a0ab721a Mon Sep 17 00:00:00 2001 From: Iury Gregory Melo Ferreira Date: Mon, 11 Dec 2023 17:18:11 -0300 Subject: [PATCH] RedfishFirmwareInterface - Unit Tests & More logs I totally missed Julia's comment in the review, this commit adds unit tests for the RedfishFirmwareInterface and also more logs when a specific component is missing. Change-Id: Ice2c946726103d9957518c2d30ddad3310ee145d --- ironic/drivers/modules/redfish/firmware.py | 30 +- .../drivers/modules/redfish/firmware_utils.py | 6 +- ironic/drivers/modules/redfish/utils.py | 11 + .../drivers/modules/redfish/test_firmware.py | 659 ++++++++++++++++++ 4 files changed, 695 insertions(+), 11 deletions(-) diff --git a/ironic/drivers/modules/redfish/firmware.py b/ironic/drivers/modules/redfish/firmware.py index c325213a6d..5379fd90fe 100644 --- a/ironic/drivers/modules/redfish/firmware.py +++ b/ironic/drivers/modules/redfish/firmware.py @@ -92,9 +92,13 @@ class RedfishFirmware(base.FirmwareInterface): system = redfish_utils.get_system(task.node) if system.bios_version: - bios_fw = {'component': 'bios', + bios_fw = {'component': redfish_utils.BIOS, 'current_version': system.bios_version} settings.append(bios_fw) + else: + LOG.debug('Could not retrieve BiosVersion in node %(node_uuid)s ' + 'system %(system)s', {'node_uuid': task.node.uuid, + 'system': system.identity}) # NOTE(iurygregory): normally we only relay on the System to # perform actions, but to retrieve the BMC Firmware we need to @@ -102,9 +106,14 @@ class RedfishFirmware(base.FirmwareInterface): try: manager = redfish_utils.get_manager(task.node, system) if manager.firmware_version: - bmc_fw = {'component': 'bmc', + bmc_fw = {'component': redfish_utils.BMC, 'current_version': manager.firmware_version} settings.append(bmc_fw) + else: + LOG.debug('Could not retrieve FirmwareVersion in node ' + '%(node_uuid)s manager %(manager)s', + {'node_uuid': task.node.uuid, + 'manager': manager.identity}) except exception.RedfishError: LOG.warning('No manager available to retrieve Firmware ' 'from the bmc of node %s', task.node.uuid) @@ -160,19 +169,17 @@ class RedfishFirmware(base.FirmwareInterface): :returns: states.CLEANWAIT if Firmware update with the settings is in progress asynchronously of None if it is complete. """ + firmware_utils.validate_firmware_interface_update_args(settings) node = task.node - update_service = redfish_utils.get_update_service(node) LOG.debug('Updating Firmware on node %(node_uuid)s with settings ' '%(settings)s', {'node_uuid': node.uuid, 'settings': settings}) - self._execute_firmware_update(node, update_service, settings) fw_upd = settings[0] wait_interval = fw_upd.get('wait') - deploy_utils.set_async_step_flags( node, reboot=True, @@ -199,8 +206,13 @@ class RedfishFirmware(base.FirmwareInterface): '%(node_uuid)s', {'url': fw_upd['url'], 'component': fw_upd['component'], 'node_uuid': node.uuid}) - - task_monitor = update_service.simple_update(component_url) + try: + task_monitor = update_service.simple_update(component_url) + except sushy.exceptions.MissingAttributeError as e: + LOG.error('The attribute #UpdateService.SimpleUpdate is missing ' + 'on node %(node)s. Error: %(error)s', + {'node': node.uuid, 'error': e.message}) + raise exception.RedfishError(error=e) fw_upd['task_monitor'] = task_monitor.task_monitor_uri node.set_driver_internal_info('redfish_fw_updates', settings) @@ -326,8 +338,7 @@ class RedfishFirmware(base.FirmwareInterface): LOG.warning('Unable to communicate with firmware update service ' 'on node %(node)s. Will try again on the next poll. ' 'Error: %(error)s', - {'node': node.uuid, - 'error': e}) + {'node': node.uuid, 'error': e}) return wait_start_time = current_update.get('wait_start_time') @@ -453,3 +464,4 @@ class RedfishFirmware(base.FirmwareInterface): except exception.IronicException: firmware_utils.cleanup(node) + raise diff --git a/ironic/drivers/modules/redfish/firmware_utils.py b/ironic/drivers/modules/redfish/firmware_utils.py index fb7ba4f7c8..f4752776fc 100644 --- a/ironic/drivers/modules/redfish/firmware_utils.py +++ b/ironic/drivers/modules/redfish/firmware_utils.py @@ -25,6 +25,7 @@ from ironic.common.i18n import _ from ironic.common import image_service from ironic.common import swift from ironic.conf import CONF +from ironic.drivers.modules.redfish import utils as redfish_utils LOG = log.getLogger(__name__) @@ -65,9 +66,10 @@ _UPDATE_FIRMWARE_SCHEMA = { } _FIRMWARE_INTERFACE_UPDATE_SCHEMA = { - "$schema": "http://json-schema.org/schema#", + "$schema": "http://json-schema.org/draft-04/schema#", "title": "update_firmware clean step schema", "type": "array", + "minItems": 1, # list of firmware update images "items": { "type": "object", @@ -76,7 +78,7 @@ _FIRMWARE_INTERFACE_UPDATE_SCHEMA = { "component": { "description": "name of the firmware component to be updated", "type": "string", - "minLenght": 1 + "enum": redfish_utils.FIRMWARE_COMPONENTS }, "url": { "description": "URL for firmware file", diff --git a/ironic/drivers/modules/redfish/utils.py b/ironic/drivers/modules/redfish/utils.py index 4182c84d11..38671e9523 100644 --- a/ironic/drivers/modules/redfish/utils.py +++ b/ironic/drivers/modules/redfish/utils.py @@ -80,6 +80,17 @@ COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy() COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES) +# All available FIRMWARE COMPONENTS +BIOS = 'bios' +"BIOS Firmware Component" + +BMC = 'bmc' +"BMC Firmware Component" + +FIRMWARE_COMPONENTS = [BIOS, BMC] +"""Firmware Components available to update""" + + def parse_driver_info(node): """Parse the information required for Ironic to connect to Redfish. diff --git a/ironic/tests/unit/drivers/modules/redfish/test_firmware.py b/ironic/tests/unit/drivers/modules/redfish/test_firmware.py index c3e984c8d2..abb832ecd7 100644 --- a/ironic/tests/unit/drivers/modules/redfish/test_firmware.py +++ b/ironic/tests/unit/drivers/modules/redfish/test_firmware.py @@ -11,10 +11,20 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime from unittest import mock from oslo_utils import importutils +from ironic.common import exception +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.modules.redfish import firmware as redfish_fw +from ironic.drivers.modules.redfish import firmware_utils +from ironic.drivers.modules.redfish import utils as redfish_utils +from ironic import objects from ironic.tests.unit.db import base as db_base from ironic.tests.unit.db import utils as db_utils from ironic.tests.unit.objects import utils as obj_utils @@ -38,3 +48,652 @@ class RedfishFirmwareTestCase(db_base.DbTestCase): enabled_firmware_interfaces=['redfish']) self.node = obj_utils.create_test_node( self.context, driver='redfish', driver_info=INFO_DICT) + + def test_get_properties(self): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + properties = task.driver.get_properties() + for prop in redfish_utils.COMMON_PROPERTIES: + self.assertIn(prop, properties) + + @mock.patch.object(redfish_utils, 'parse_driver_info', autospec=True) + def test_validate(self, mock_parse_driver_info): + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.driver.firmware.validate(task) + mock_parse_driver_info.assert_called_once_with(task.node) + + @mock.patch.object(redfish_fw, 'LOG', autospec=True) + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + @mock.patch.object(redfish_utils, 'get_manager', autospec=True) + @mock.patch.object(objects.FirmwareComponentList, + 'sync_firmware_components', autospec=True) + def test_missing_all_components(self, sync_fw_cmp_mock, manager_mock, + system_mock, log_mock): + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + system_mock.return_value.identity = "System1" + manager_mock.return_value.identity = "Manager1" + system_mock.return_value.bios_version = None + manager_mock.return_value.firmware_version = None + + self.assertRaises(exception.UnsupportedDriverExtension, + task.driver.firmware.cache_firmware_components, + task) + + sync_fw_cmp_mock.assert_not_called() + error_msg = ( + 'Cannot retrieve firmware for node %s: ' + 'no supported components' + % self.node.uuid) + log_mock.error.assert_called_once_with(error_msg) + + warning_calls = [ + mock.call('Could not retrieve BiosVersion in node ' + '%(node_uuid)s system %(system)s', + {'node_uuid': self.node.uuid, + 'system': "System1"}), + mock.call('Could not retrieve FirmwareVersion in node ' + '%(node_uuid)s manager %(manager)s', + {'node_uuid': self.node.uuid, + 'manager': "Manager1"})] + log_mock.debug.assert_has_calls(warning_calls) + + @mock.patch.object(redfish_fw, 'LOG', autospec=True) + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + @mock.patch.object(redfish_utils, 'get_manager', autospec=True) + @mock.patch.object(objects.FirmwareComponentList, + 'sync_firmware_components', autospec=True) + @mock.patch.object(objects, 'FirmwareComponent', spec_set=True, + autospec=True) + def test_missing_bios_component(self, fw_cmp_mock, sync_fw_cmp_mock, + manager_mock, system_mock, log_mock): + create_list = [{'component': 'bmc', 'current_version': 'v1.0.0'}] + sync_fw_cmp_mock.return_value = ( + create_list, [], [] + ) + + bmc_component = {'component': 'bmc', 'current_version': 'v1.0.0', + 'node_id': self.node.id} + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + system_mock.return_value.identity = "System1" + system_mock.return_value.bios_version = None + manager_mock.return_value.firmware_version = "v1.0.0" + + task.driver.firmware.cache_firmware_components(task) + system_mock.assert_called_once_with(task.node) + + log_mock.debug.assert_called_once_with( + 'Could not retrieve BiosVersion in node ' + '%(node_uuid)s system %(system)s', + {'node_uuid': self.node.uuid, 'system': 'System1'}) + sync_fw_cmp_mock.assert_called_once_with( + task.context, task.node.id, + [{'component': 'bmc', 'current_version': 'v1.0.0'}]) + self.assertTrue(fw_cmp_mock.called) + fw_cmp_mock.assert_called_once_with(task.context, **bmc_component) + + @mock.patch.object(redfish_fw, 'LOG', autospec=True) + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + @mock.patch.object(redfish_utils, 'get_manager', autospec=True) + @mock.patch.object(objects.FirmwareComponentList, + 'sync_firmware_components', autospec=True) + @mock.patch.object(objects, 'FirmwareComponent', spec_set=True, + autospec=True) + def test_missing_bmc_component(self, fw_cmp_mock, sync_fw_cmp_mock, + manager_mock, system_mock, log_mock): + create_list = [{'component': 'bios', 'current_version': 'v1.0.0'}] + sync_fw_cmp_mock.return_value = ( + create_list, [], [] + ) + + bios_component = {'component': 'bios', 'current_version': 'v1.0.0', + 'node_id': self.node.id} + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + manager_mock.return_value.identity = "Manager1" + manager_mock.return_value.firmware_version = None + system_mock.return_value.bios_version = "v1.0.0" + task.driver.firmware.cache_firmware_components(task) + + log_mock.debug.assert_called_once_with( + 'Could not retrieve FirmwareVersion in node ' + '%(node_uuid)s manager %(manager)s', + {'node_uuid': self.node.uuid, 'manager': "Manager1"}) + system_mock.assert_called_once_with(task.node) + sync_fw_cmp_mock.assert_called_once_with( + task.context, task.node.id, + [{'component': 'bios', 'current_version': 'v1.0.0'}]) + self.assertTrue(fw_cmp_mock.called) + fw_cmp_mock.assert_called_once_with(task.context, **bios_component) + + @mock.patch.object(redfish_fw, 'LOG', autospec=True) + @mock.patch.object(redfish_utils, 'get_system', autospec=True) + @mock.patch.object(redfish_utils, 'get_manager', autospec=True) + @mock.patch.object(objects, 'FirmwareComponentList', autospec=True) + @mock.patch.object(objects, 'FirmwareComponent', spec_set=True, + autospec=True) + def test_create_all_components(self, fw_cmp_mock, fw_cmp_list_mock, + manager_mock, system_mock, log_mock): + create_list = [{'component': 'bios', 'current_version': 'v1.0.0'}, + {'component': 'bmc', 'current_version': 'v1.0.0'}] + fw_cmp_list_mock.sync_firmware_components.return_value = ( + create_list, [], [] + ) + + bios_component = {'component': 'bios', 'current_version': 'v1.0.0', + 'node_id': self.node.id} + + bmc_component = {'component': 'bmc', 'current_version': 'v1.0.0', + 'node_id': self.node.id} + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + manager_mock.return_value.firmware_version = "v1.0.0" + system_mock.return_value.bios_version = "v1.0.0" + task.driver.firmware.cache_firmware_components(task) + + log_mock.warning.assert_not_called() + log_mock.debug.assert_not_called() + system_mock.assert_called_once_with(task.node) + fw_cmp_list_mock.sync_firmware_components.assert_called_once_with( + task.context, task.node.id, + [{'component': 'bios', 'current_version': 'v1.0.0'}, + {'component': 'bmc', 'current_version': 'v1.0.0'}]) + fw_cmp_calls = [ + mock.call(task.context, **bios_component), + mock.call().create(), + mock.call(task.context, **bmc_component), + mock.call().create(), + ] + fw_cmp_mock.assert_has_calls(fw_cmp_calls) + + @mock.patch.object(redfish_utils, 'LOG', autospec=True) + @mock.patch.object(redfish_utils, '_get_connection', autospec=True) + def test_missing_updateservice(self, conn_mock, log_mock): + settings = [{'component': 'bmc', 'url': 'http://upfwbmc/v2.0.0'}] + conn_mock.side_effect = sushy.exceptions.MissingAttributeError( + attribute='UpdateService', resource='redfish/v1') + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + error_msg = ('The attribute UpdateService is missing from the ' + 'resource redfish/v1') + self.assertRaisesRegex( + exception.RedfishError, error_msg, + task.driver.firmware.update, + task, settings) + + @mock.patch.object(redfish_fw, 'LOG', autospec=True) + @mock.patch.object(redfish_utils, 'get_update_service', autospec=True) + def test_missing_simple_update_action(self, update_service_mock, log_mock): + settings = [{'component': 'bmc', 'url': 'http://upfwbmc/v2.0.0'}] + update_service = update_service_mock.return_value + update_service.simple_update.side_effect = \ + sushy.exceptions.MissingAttributeError( + attribute='#UpdateService.SimpleUpdate', + resource='redfish/v1/UpdateService') + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + + self.assertRaises( + exception.RedfishError, + task.driver.firmware.update, + task, settings) + expected_err_msg = ( + 'The attribute #UpdateService.SimpleUpdate is missing ' + 'from the resource redfish/v1/UpdateService') + log_mock.error.assert_called_once_with( + 'The attribute #UpdateService.SimpleUpdate is missing ' + 'on node %(node)s. Error: %(error)s', + {'node': self.node.uuid, 'error': expected_err_msg}) + + component = settings[0].get('component') + url = settings[0].get('url') + + log_call = [ + mock.call('Updating Firmware on node %(node_uuid)s ' + 'with settings %(settings)s', + {'node_uuid': self.node.uuid, + 'settings': settings}), + mock.call('For node %(node)s serving firmware for ' + '%(component)s from original location %(url)s', + {'node': self.node.uuid, + 'component': component, 'url': url}), + mock.call('Applying new firmware %(url)s for ' + '%(component)s on node %(node_uuid)s', + {'url': url, 'component': component, + 'node_uuid': self.node.uuid}) + ] + log_mock.debug.assert_has_calls(log_call) + + @mock.patch.object(redfish_fw, 'LOG', autospec=True) + def _test_invalid_settings(self, log_mock): + step = self.node.clean_step + settings = step['argsinfo'].get('settings', None) + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises( + exception.InvalidParameterValue, + task.driver.firmware.update, + task, settings) + log_mock.debug.assert_not_called() + + def test_invalid_component_in_settings(self): + argsinfo = {'settings': [ + {'component': 'nic', 'url': 'https://nic-update/v1.1.0'} + ]} + self.node.clean_step = {'priority': 100, 'interface': 'firmware', + 'step': 'update', + 'argsinfo': argsinfo} + self.node.save() + self._test_invalid_settings() + + def test_missing_required_field_in_settings(self): + argsinfo = {'settings': [ + {'url': 'https://nic-update/v1.1.0'}, + {'component': "bmc"} + ]} + self.node.clean_step = {'priority': 100, 'interface': 'firmware', + 'step': 'update', + 'argsinfo': argsinfo} + self.node.save() + self._test_invalid_settings() + + def test_empty_settings(self): + argsinfo = {'settings': []} + self.node.clean_step = {'priority': 100, 'interface': 'firmware', + 'step': 'update', + 'argsinfo': argsinfo} + self.node.save() + self._test_invalid_settings() + + def _generate_new_driver_internal_info(self, components=[], invalid=False, + add_wait=False, wait=1): + bmc_component = {'component': 'bmc', 'url': 'https://bmc/v1.0.1'} + bios_component = {'component': 'bios', 'url': 'https://bios/v1.0.1'} + if add_wait: + wait_start_time = datetime.datetime.utcnow() -\ + datetime.timedelta(minutes=1) + bmc_component['wait_start_time'] = wait_start_time.isoformat() + bios_component['wait_start_time'] = wait_start_time.isoformat() + bmc_component['wait'] = wait + bios_component['wait'] = wait + + self.node.clean_step = {'priority': 100, 'interface': 'bios', + 'step': 'apply_configuration', + 'argsinfo': {'settings': []}} + + updates = [] + if 'bmc' in components: + self.node.clean_step['argsinfo']['settings'].append( + bmc_component) + bmc_component['task_monitor'] = '/task/1' + updates.append(bmc_component) + if 'bios' in components: + self.node.clean_step['argsinfo']['settings'].append( + bios_component) + bios_component['task_monitor'] = '/task/2' + updates.append(bios_component) + + if invalid: + self.node.provision_state = states.CLEANING + self.node.driver_internal_info = {'something': 'else'} + else: + self.node.provision_state = states.CLEANING + self.node.driver_internal_info = { + 'redfish_fw_updates': updates, + } + self.node.save() + + @mock.patch.object(task_manager, 'acquire', autospec=True) + def _test__query_methods(self, acquire_mock): + firmware = redfish_fw.RedfishFirmware() + mock_manager = mock.Mock() + node_list = [(self.node.uuid, 'redfish', '', + self.node.driver_internal_info)] + mock_manager.iter_nodes.return_value = node_list + task = mock.Mock(node=self.node, + driver=mock.Mock(firmware=firmware)) + acquire_mock.return_value = mock.MagicMock( + __enter__=mock.MagicMock(return_value=task)) + + firmware._check_node_redfish_firmware_update = mock.Mock() + firmware._clear_updates = mock.Mock() + + # _query_update_status + firmware._query_update_status(mock_manager, self.context) + if not self.node.driver_internal_info.get('redfish_fw_updates'): + firmware._check_node_redfish_firmware_update.assert_not_called() + else: + firmware._check_node_redfish_firmware_update.\ + assert_called_once_with(task) + + # _query_update_failed + firmware._query_update_failed(mock_manager, self.context) + if not self.node.driver_internal_info.get('redfish_fw_updates'): + firmware._clear_updates.assert_not_called() + else: + firmware._clear_updates.assert_called_once_with(self.node) + + def test_redfish_fw_updates(self): + self._generate_new_driver_internal_info(['bmc']) + self._test__query_methods() + + def test_redfish_fw_updates_empty(self): + self._generate_new_driver_internal_info(invalid=True) + self._test__query_methods() + + def _test__check_node_redfish_firmware_update(self): + firmware = redfish_fw.RedfishFirmware() + firmware._continue_updates = mock.Mock() + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + task.upgrade_lock = mock.Mock() + task.process_event = mock.Mock() + firmware._check_node_redfish_firmware_update(task) + return task, firmware + + @mock.patch.object(redfish_fw, 'LOG', autospec=True) + @mock.patch.object(redfish_utils, 'get_update_service', autospec=True) + def test_check_conn_error(self, get_us_mock, log_mock): + self._generate_new_driver_internal_info(['bmc']) + get_us_mock.side_effect = exception.RedfishConnectionError('Error') + try: + self._test__check_node_redfish_firmware_update() + except exception.RedfishError as e: + exception_error = e.kwargs.get('error') + + warning_calls = [ + mock.call('Unable to communicate with firmware update ' + 'service on node %(node)s. Will try again on ' + 'the next poll. Error: %(error)s', + {'node': self.node.uuid, + 'error': exception_error}) + ] + log_mock.warning.assert_has_calls(warning_calls) + + @mock.patch.object(redfish_fw, 'LOG', autospec=True) + @mock.patch.object(redfish_utils, 'get_update_service', autospec=True) + def test_check_update_wait_elapsed(self, get_us_mock, log_mock): + mock_update_service = mock.Mock() + get_us_mock.return_value = mock_update_service + self._generate_new_driver_internal_info(['bmc'], add_wait=True) + + task, interface = self._test__check_node_redfish_firmware_update() + debug_calls = [ + mock.call('Finished waiting after firmware update ' + '%(firmware_image)s on node %(node)s. ' + 'Elapsed time: %(seconds)s seconds', + {'firmware_image': 'https://bmc/v1.0.1', + 'node': self.node.uuid, 'seconds': 60})] + log_mock.debug.assert_has_calls(debug_calls) + interface._continue_updates.assert_called_once_with( + task, + mock_update_service, + [{'component': 'bmc', 'url': 'https://bmc/v1.0.1', + 'task_monitor': '/task/1'}]) + + @mock.patch.object(redfish_fw, 'LOG', autospec=True) + @mock.patch.object(redfish_utils, 'get_update_service', autospec=True) + def test_check_update_still_waiting(self, get_us_mock, log_mock): + mock_update_service = mock.Mock() + get_us_mock.return_value = mock_update_service + self._generate_new_driver_internal_info( + ['bios'], add_wait=True, wait=600) + + _, interface = self._test__check_node_redfish_firmware_update() + debug_calls = [ + mock.call('Continuing to wait after firmware update ' + '%(firmware_image)s on node %(node)s. ' + 'Elapsed time: %(seconds)s seconds', + {'firmware_image': 'https://bios/v1.0.1', + 'node': self.node.uuid, 'seconds': 60})] + log_mock.debug.assert_has_calls(debug_calls) + interface._continue_updates.assert_not_called() + + @mock.patch.object(redfish_fw, 'LOG', autospec=True) + @mock.patch.object(redfish_utils, 'get_update_service', autospec=True) + @mock.patch.object(redfish_utils, 'get_task_monitor', autospec=True) + def test_check_update_task_monitor_not_found(self, tm_mock, get_us_mock, + log_mock): + tm_mock.side_effect = exception.RedfishError() + self._generate_new_driver_internal_info(['bios']) + + task, interface = self._test__check_node_redfish_firmware_update() + warning_calls = [ + mock.call('Firmware update completed for node %(node)s, ' + 'firmware %(firmware_image)s, but success of the ' + 'update is unknown. Assuming update was successful.', + {'node': self.node.uuid, + 'firmware_image': 'https://bios/v1.0.1'})] + + log_mock.warning.assert_has_calls(warning_calls) + interface._continue_updates.assert_called_once_with( + task, get_us_mock.return_value, + [{'component': 'bios', 'url': 'https://bios/v1.0.1', + 'task_monitor': '/task/2'}] + ) + + @mock.patch.object(redfish_fw, 'LOG', autospec=True) + @mock.patch.object(redfish_utils, 'get_update_service', autospec=True) + @mock.patch.object(redfish_utils, 'get_task_monitor', autospec=True) + def test__check_update_in_progress(self, tm_mock, get_us_mock, log_mock): + tm_mock.return_value.is_processing = True + self._generate_new_driver_internal_info(['bmc']) + + _, interface = self._test__check_node_redfish_firmware_update() + debug_calls = [ + mock.call('Firmware update in progress for node %(node)s, ' + 'firmware %(firmware_image)s.', + {'node': self.node.uuid, + 'firmware_image': 'https://bmc/v1.0.1'})] + + log_mock.debug.assert_has_calls(debug_calls) + + interface._continue_updates.assert_not_called() + + @mock.patch.object(manager_utils, 'cleaning_error_handler', autospec=True) + @mock.patch.object(redfish_utils, 'get_update_service', autospec=True) + @mock.patch.object(redfish_utils, 'get_task_monitor', autospec=True) + def test__check_node_firmware_update_fail(self, tm_mock, get_us_mock, + cleaning_error_handler_mock): + mock_sushy_task = mock.Mock() + mock_sushy_task.task_state = 'exception' + mock_message_unparsed = mock.Mock() + mock_message_unparsed.message = None + message_mock = mock.Mock() + message_mock.message = 'Firmware upgrade failed' + messages = mock.MagicMock(return_value=[[mock_message_unparsed], + [message_mock], + [message_mock]]) + mock_sushy_task.messages = messages + mock_task_monitor = mock.Mock() + mock_task_monitor.is_processing = False + mock_task_monitor.get_task.return_value = mock_sushy_task + tm_mock.return_value = mock_task_monitor + self._generate_new_driver_internal_info(['bmc']) + + task, interface = self._test__check_node_redfish_firmware_update() + + task.upgrade_lock.assert_called_once_with() + cleaning_error_handler_mock.assert_called_once() + interface._continue_updates.assert_not_called() + + @mock.patch.object(redfish_fw, 'LOG', autospec=True) + @mock.patch.object(redfish_utils, 'get_update_service', autospec=True) + @mock.patch.object(redfish_utils, 'get_task_monitor', autospec=True) + def test__check_node_firmware_update_done(self, tm_mock, get_us_mock, + log_mock): + task_mock = mock.Mock() + task_mock.task_state = sushy.TASK_STATE_COMPLETED + task_mock.task_status = sushy.HEALTH_OK + message_mock = mock.Mock() + message_mock.message = 'Firmware update done' + task_mock.messages = [message_mock] + mock_task_monitor = mock.Mock() + mock_task_monitor.is_processing = False + mock_task_monitor.get_task.return_value = task_mock + tm_mock.return_value = mock_task_monitor + self._generate_new_driver_internal_info(['bmc']) + + task, interface = self._test__check_node_redfish_firmware_update() + task.upgrade_lock.assert_called_once_with() + info_calls = [ + mock.call('Firmware update succeeded for node %(node)s, ' + 'firmware %(firmware_image)s: %(messages)s', + {'node': self.node.uuid, + 'firmware_image': 'https://bmc/v1.0.1', + 'messages': 'Firmware update done'})] + + log_mock.info.assert_has_calls(info_calls) + + interface._continue_updates.assert_called_once_with( + task, get_us_mock.return_value, + [{'component': 'bmc', 'url': 'https://bmc/v1.0.1', + 'task_monitor': '/task/1'}] + ) + + @mock.patch.object(firmware_utils, 'download_to_temp', autospec=True) + @mock.patch.object(firmware_utils, 'stage', autospec=True) + def test__stage_firmware_file_https(self, stage_mock, dwl_tmp_mock): + CONF.set_override('firmware_source', 'local', 'redfish') + firmware_update = {'url': 'https://test1', 'component': 'bmc'} + node = mock.Mock() + dwl_tmp_mock.return_value = '/tmp/test1' + stage_mock.return_value = ('http://staged/test1', 'http') + + firmware = redfish_fw.RedfishFirmware() + + staged_url, needs_cleanup = firmware._stage_firmware_file( + node, firmware_update) + + self.assertEqual(staged_url, 'http://staged/test1') + self.assertEqual(needs_cleanup, 'http') + dwl_tmp_mock.assert_called_with(node, 'https://test1') + stage_mock.assert_called_with(node, 'local', '/tmp/test1') + + @mock.patch.object(firmware_utils, 'download_to_temp', autospec=True) + @mock.patch.object(firmware_utils, 'stage', autospec=True) + @mock.patch.object(firmware_utils, 'get_swift_temp_url', autospec=True) + def test__stage_firmware_file_swift( + self, get_swift_tmp_url_mock, stage_mock, dwl_tmp_mock): + CONF.set_override('firmware_source', 'swift', 'redfish') + firmware_update = {'url': 'swift://container/bios.exe', + 'component': 'bios'} + node = mock.Mock() + get_swift_tmp_url_mock.return_value = 'http://temp' + + firmware = redfish_fw.RedfishFirmware() + + staged_url, needs_cleanup = firmware._stage_firmware_file( + node, firmware_update) + + self.assertEqual(staged_url, 'http://temp') + self.assertIsNone(needs_cleanup) + dwl_tmp_mock.assert_not_called() + stage_mock.assert_not_called() + + @mock.patch.object(firmware_utils, 'cleanup', autospec=True) + @mock.patch.object(firmware_utils, 'download_to_temp', autospec=True) + @mock.patch.object(firmware_utils, 'stage', autospec=True) + def test__stage_firmware_file_error(self, stage_mock, dwl_tmp_mock, + cleanup_mock): + CONF.set_override('firmware_source', 'local', 'redfish') + node = mock.Mock() + firmware_update = {'url': 'https://test1', 'component': 'bmc'} + dwl_tmp_mock.return_value = '/tmp/test1' + stage_mock.side_effect = exception.IronicException + + firmware = redfish_fw.RedfishFirmware() + self.assertRaises(exception.IronicException, + firmware._stage_firmware_file, node, + firmware_update) + dwl_tmp_mock.assert_called_with(node, 'https://test1') + stage_mock.assert_called_with(node, 'local', '/tmp/test1') + cleanup_mock.assert_called_with(node) + + def _test_continue_updates(self): + + update_service_mock = mock.Mock() + firmware = redfish_fw.RedfishFirmware() + + updates = self.node.driver_internal_info.get('redfish_fw_updates') + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + firmware._continue_updates( + task, + update_service_mock, + updates + ) + return task + + @mock.patch.object(redfish_fw, 'LOG', autospec=True) + def test_continue_update_waitting(self, log_mock): + self._generate_new_driver_internal_info(['bmc', 'bios'], + add_wait=True, wait=120) + self._test_continue_updates() + debug_call = [ + mock.call('Waiting at %(time)s for %(seconds)s seconds ' + 'after %(component)s firmware update %(url)s ' + 'on node %(node)s', + {'time': mock.ANY, 'seconds': 120, + 'component': 'bmc', 'url': 'https://bmc/v1.0.1', + 'node': self.node.uuid}) + ] + log_mock.debug.assert_has_calls(debug_call) + + @mock.patch.object(redfish_fw, 'LOG', autospec=True) + @mock.patch.object(manager_utils, 'notify_conductor_resume_clean', + autospec=True) + def test_continue_updates_last(self, cond_resume_clean_mock, log_mock): + self._generate_new_driver_internal_info(['bmc']) + task = self._test_continue_updates() + + cond_resume_clean_mock.assert_called_once_with(task) + + info_call = [ + mock.call('Firmware updates completed for node %(node)s', + {'node': self.node.uuid}) + ] + log_mock.info.assert_has_calls(info_call) + + @mock.patch.object(redfish_fw, 'LOG', autospec=True) + @mock.patch.object(manager_utils, 'node_power_action', autospec=True) + def test_continue_updates_more_updates(self, node_power_action_mock, + log_mock): + self._generate_new_driver_internal_info(['bmc', 'bios']) + + task_monitor_mock = mock.Mock() + task_monitor_mock.task_monitor_uri = '/task/2' + update_service_mock = mock.Mock() + update_service_mock.simple_update.return_value = task_monitor_mock + + firmware = redfish_fw.RedfishFirmware() + updates = self.node.driver_internal_info.get('redfish_fw_updates') + + with task_manager.acquire(self.context, self.node.uuid, + shared=True) as task: + task.node.save = mock.Mock() + + firmware._continue_updates(task, update_service_mock, updates) + + debug_calls = [ + mock.call('Applying new firmware %(url)s for ' + '%(component)s on node %(node_uuid)s', + {'url': 'https://bios/v1.0.1', 'component': 'bios', + 'node_uuid': self.node.uuid}) + ] + log_mock.debug.assert_has_calls(debug_calls) + self.assertEqual( + [{'component': 'bios', 'url': 'https://bios/v1.0.1', + 'task_monitor': '/task/2'}], + task.node.driver_internal_info['redfish_fw_updates']) + update_service_mock.simple_update.assert_called_once_with( + 'https://bios/v1.0.1') + task.node.save.assert_called_once_with() + node_power_action_mock.assert_called_once_with(task, states.REBOOT)