When doing firmware updates for BMC, we saw cases where Ironic wouldn't be able to contact the BMC, marking the node in a failed state because of it. This patch adds a configuration option that tell for how long ironic should wait before proceeding with the reboot to finish the update. We will attempt to improve the waiting time in a follow-up, trying to identify when the bmc was unresponsive and when it was back. Closes-Bug: #2092398 Change-Id: I53ffc8a06d5af8b0751553c3d4a9bb1c000027ae Signed-off-by: Iury Gregory Melo Ferreira <imelofer@redhat.com>
964 lines
44 KiB
Python
964 lines
44 KiB
Python
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import datetime
|
|
import json
|
|
import time
|
|
from unittest import mock
|
|
|
|
from oslo_config import cfg
|
|
from oslo_utils import timeutils
|
|
import sushy
|
|
|
|
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
|
|
|
|
INFO_DICT = db_utils.get_test_redfish_info()
|
|
|
|
|
|
class RedfishFirmwareTestCase(db_base.DbTestCase):
|
|
|
|
def setUp(self):
|
|
super(RedfishFirmwareTestCase, self).setUp()
|
|
self.config(enabled_bios_interfaces=['redfish'],
|
|
enabled_hardware_types=['redfish'],
|
|
enabled_power_interfaces=['redfish'],
|
|
enabled_boot_interfaces=['redfish-virtual-media'],
|
|
enabled_management_interfaces=['redfish'],
|
|
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)
|
|
@mock.patch.object(redfish_utils, 'get_system_collection', autospec=True)
|
|
def test_missing_simple_update_action(self, get_systems_collection_mock,
|
|
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()
|
|
|
|
@mock.patch.object(redfish_fw, 'LOG', autospec=True)
|
|
def _test_invalid_settings_service(self, log_mock):
|
|
step = self.node.service_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_invalid_component_in_settings_service(self):
|
|
argsinfo = {'settings': [
|
|
{'component': 'nic', 'url': 'https://nic-update/v1.1.0'}
|
|
]}
|
|
self.node.service_step = {'priority': 100, 'interface': 'firmware',
|
|
'step': 'update',
|
|
'argsinfo': argsinfo}
|
|
self.node.save()
|
|
self._test_invalid_settings_service()
|
|
|
|
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_missing_required_field_in_settings_service(self):
|
|
argsinfo = {'settings': [
|
|
{'url': 'https://nic-update/v1.1.0'},
|
|
{'component': "bmc"}
|
|
]}
|
|
self.node.service_step = {'priority': 100, 'interface': 'firmware',
|
|
'step': 'update',
|
|
'argsinfo': argsinfo}
|
|
self.node.save()
|
|
self._test_invalid_settings_service()
|
|
|
|
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 test_empty_settings_service(self):
|
|
argsinfo = {'settings': []}
|
|
self.node.service_step = {'priority': 100, 'interface': 'firmware',
|
|
'step': 'update',
|
|
'argsinfo': argsinfo}
|
|
self.node.save()
|
|
self._test_invalid_settings_service()
|
|
|
|
@mock.patch.object(time, 'sleep', lambda seconds: None)
|
|
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 = timeutils.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()
|
|
|
|
def _generate_new_driver_internal_info_service(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 = timeutils.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.service_step = {'priority': 100, 'interface': 'bios',
|
|
'step': 'apply_configuration',
|
|
'argsinfo': {'settings': []}}
|
|
|
|
updates = []
|
|
if 'bmc' in components:
|
|
self.node.service_step['argsinfo']['settings'].append(
|
|
bmc_component)
|
|
bmc_component['task_monitor'] = '/task/1'
|
|
updates.append(bmc_component)
|
|
if 'bios' in components:
|
|
self.node.service_step['argsinfo']['settings'].append(
|
|
bios_component)
|
|
bios_component['task_monitor'] = '/task/2'
|
|
updates.append(bios_component)
|
|
|
|
if invalid:
|
|
self.node.provision_state = states.SERVICING
|
|
self.node.driver_internal_info = {'something': 'else'}
|
|
else:
|
|
self.node.provision_state = states.SERVICING
|
|
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(manager_utils, 'servicing_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_servicing(
|
|
self, tm_mock,
|
|
get_us_mock,
|
|
servicing_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_service(['bmc'])
|
|
|
|
task, interface = self._test__check_node_redfish_firmware_update()
|
|
|
|
task.upgrade_lock.assert_called_once_with()
|
|
servicing_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, 'notify_conductor_resume_service',
|
|
autospec=True)
|
|
def test_continue_updates_last_service(self, cond_resume_service_mock,
|
|
log_mock):
|
|
self._generate_new_driver_internal_info_service(['bmc'])
|
|
task = self._test_continue_updates()
|
|
|
|
cond_resume_service_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)
|
|
@mock.patch.object(redfish_utils, 'get_system_collection', autospec=True)
|
|
def test_continue_updates_more_updates(self, get_system_collection_mock,
|
|
node_power_action_mock,
|
|
log_mock):
|
|
cfg.CONF.set_override('firmware_update_wait_unresponsive_bmc', 0,
|
|
'redfish')
|
|
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)
|
|
|
|
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
|
|
@mock.patch.object(redfish_utils, 'get_system_collection', autospec=True)
|
|
@mock.patch.object(time, 'sleep', lambda seconds: None)
|
|
def test__execute_firmware_update_no_targets(self,
|
|
get_system_collection_mock,
|
|
system_mock):
|
|
self._generate_new_driver_internal_info(['bios'])
|
|
with open('ironic/tests/json_samples/'
|
|
'systems_collection_single.json') as f:
|
|
response_obj = json.load(f)
|
|
system_collection_mock = mock.MagicMock()
|
|
system_collection_mock.get_members.return_value = response_obj[
|
|
'Members']
|
|
get_system_collection_mock.return_value = system_collection_mock
|
|
|
|
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()
|
|
|
|
settings = [{'component': 'bios', 'url': 'https://bios/v1.0.1'}]
|
|
firmware._execute_firmware_update(self.node, update_service_mock,
|
|
settings)
|
|
update_service_mock.simple_update.assert_called_once_with(
|
|
'https://bios/v1.0.1')
|
|
|
|
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
|
|
@mock.patch.object(redfish_utils, 'get_system_collection', autospec=True)
|
|
@mock.patch.object(time, 'sleep', lambda seconds: None)
|
|
def test__execute_firmware_update_targets(self,
|
|
get_system_collection_mock,
|
|
system_mock):
|
|
self._generate_new_driver_internal_info(['bios'])
|
|
with open('ironic/tests/json_samples/'
|
|
'systems_collection_dual.json') as f:
|
|
response_obj = json.load(f)
|
|
system_collection_mock = mock.MagicMock()
|
|
system_collection_mock.members_identities = response_obj[
|
|
'Members']
|
|
get_system_collection_mock.return_value = system_collection_mock
|
|
|
|
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()
|
|
|
|
settings = [{'component': 'bios', 'url': 'https://bios/v1.0.1'}]
|
|
firmware._execute_firmware_update(self.node, update_service_mock,
|
|
settings)
|
|
update_service_mock.simple_update.assert_called_once_with(
|
|
'https://bios/v1.0.1', targets=[mock.ANY])
|
|
|
|
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
|
|
@mock.patch.object(redfish_utils, 'get_system_collection', autospec=True)
|
|
@mock.patch.object(time, 'sleep', autospec=True)
|
|
def test__execute_firmware_update_unresponsive_bmc(self, sleep_mock,
|
|
get_sys_collec_mock,
|
|
system_mock):
|
|
cfg.CONF.set_override('firmware_update_wait_unresponsive_bmc', 1,
|
|
'redfish')
|
|
self._generate_new_driver_internal_info(['bmc'])
|
|
with open(
|
|
'ironic/tests/json_samples/systems_collection_single.json'
|
|
) as f:
|
|
resp_obj = json.load(f)
|
|
system_collection_mock = mock.MagicMock()
|
|
system_collection_mock.get_members.return_value = resp_obj['Members']
|
|
get_sys_collec_mock.return_value = system_collection_mock
|
|
|
|
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()
|
|
|
|
settings = [{'component': 'bmc', 'url': 'https://bmc/v1.2.3'}]
|
|
firmware._execute_firmware_update(self.node, update_service_mock,
|
|
settings)
|
|
|
|
update_service_mock.simple_update.assert_called_once_with(
|
|
'https://bmc/v1.2.3')
|
|
sleep_mock.assert_called_once_with(
|
|
CONF.redfish.firmware_update_wait_unresponsive_bmc)
|
|
|
|
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
|
|
@mock.patch.object(redfish_utils, 'get_system_collection', autospec=True)
|
|
@mock.patch.object(time, 'sleep', autospec=True)
|
|
def test__execute_firmware_update_unresponsive_bmc_node_override(
|
|
self, sleep_mock, get_sys_collec_mock, system_mock):
|
|
self._generate_new_driver_internal_info(['bmc'])
|
|
# Set a specific value for firmware_update_unresponsive_bmc_wait for
|
|
# the node
|
|
with mock.patch('time.sleep', lambda x: None):
|
|
d_info = self.node.driver_info.copy()
|
|
d_info['firmware_update_unresponsive_bmc_wait'] = 1
|
|
self.node.driver_info = d_info
|
|
self.node.save()
|
|
|
|
self.assertNotEqual(
|
|
CONF.redfish.firmware_update_wait_unresponsive_bmc,
|
|
self.node.driver_info.get('firmware_update_unresponsive_bmc_wait')
|
|
)
|
|
|
|
with open(
|
|
'ironic/tests/json_samples/systems_collection_single.json'
|
|
) as f:
|
|
resp_obj = json.load(f)
|
|
system_collection_mock = mock.MagicMock()
|
|
system_collection_mock.get_members.return_value = resp_obj['Members']
|
|
get_sys_collec_mock.return_value = system_collection_mock
|
|
|
|
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()
|
|
settings = [{'component': 'bmc', 'url': 'https://bmc/v1.2.3'}]
|
|
firmware._execute_firmware_update(self.node, update_service_mock,
|
|
settings)
|
|
|
|
update_service_mock.simple_update.assert_called_once_with(
|
|
'https://bmc/v1.2.3')
|
|
sleep_mock.assert_called_once_with(
|
|
self.node.driver_info.get('firmware_update_unresponsive_bmc_wait')
|
|
)
|