From eae33a0acbfdbe30b5d79360e76668737866e371 Mon Sep 17 00:00:00 2001 From: Vanou Ishii Date: Tue, 8 Nov 2022 21:18:13 -0500 Subject: [PATCH] [iRMC] identify BMC firmware version Since iRMC S6 2.00, iRMC firmware doesn't support HTTP connection to REST API. To deal with this firmware incompatibility, this commit adds verify step to check connection to REST API and adds node vendor passthru to fetch&cache version of iRMC firmware. Story: 2010396 Task: 46745 Change-Id: Ib04b66b0c7b1ef1c4175841689c16a7fbc0b1e54 --- doc/source/admin/drivers/irmc.rst | 19 ++ ironic/drivers/irmc.py | 6 + ironic/drivers/modules/irmc/common.py | 212 ++++++++++++++++++ ironic/drivers/modules/irmc/management.py | 38 ++++ ironic/drivers/modules/irmc/vendor.py | 75 +++++++ .../unit/drivers/modules/irmc/test_common.py | 129 +++++++++++ .../drivers/modules/irmc/test_management.py | 90 ++++++++ .../drivers/third_party_driver_mock_specs.py | 2 + ...http-incompatibility-61a31d12aa33fbd8.yaml | 19 ++ setup.cfg | 1 + 10 files changed, 591 insertions(+) create mode 100644 ironic/drivers/modules/irmc/vendor.py create mode 100644 releasenotes/notes/fix-irmc-s6-2.00-http-incompatibility-61a31d12aa33fbd8.yaml diff --git a/doc/source/admin/drivers/irmc.rst b/doc/source/admin/drivers/irmc.rst index 83d7eccb38..dfb61e2275 100644 --- a/doc/source/admin/drivers/irmc.rst +++ b/doc/source/admin/drivers/irmc.rst @@ -210,6 +210,25 @@ Configuration via ``ironic.conf`` - ``port``: Port to be used for iRMC operations; either 80 or 443. The default value is 443. Optional. + + .. note:: + Since iRMC S6 2.00, iRMC firmware doesn't support HTTP connection to + REST API. If you deploy server with iRMS S6 2.00 and later, please + set ``port`` to 443. + + ``irmc`` hardware type provides ``verify_step`` named + ``verify_http_https_connection_and_fw_version`` to check HTTP(S) + connection to iRMC REST API. If HTTP(S) connection is successfully + established, then it fetches and caches iRMC firmware version. + If HTTP(S) connection to iRMC REST API failed, Ironic node's state + moves to ``enroll`` with suggestion put in log message. + Default priority of this verify step is 10. + + If operator updates iRMC firmware version of node, operator should + run ``cache_irmc_firmware_version`` node vendor passthru method + to update iRMC firmware version stored in + ``driver_internal_info/irmc_fw_version``. + - ``auth_method``: Authentication method for iRMC operations; either ``basic`` or ``digest``. The default value is ``basic``. Optional. - ``client_timeout``: Timeout (in seconds) for iRMC diff --git a/ironic/drivers/irmc.py b/ironic/drivers/irmc.py index 50bb9114d8..06408359b8 100644 --- a/ironic/drivers/irmc.py +++ b/ironic/drivers/irmc.py @@ -27,6 +27,7 @@ from ironic.drivers.modules.irmc import inspect from ironic.drivers.modules.irmc import management from ironic.drivers.modules.irmc import power from ironic.drivers.modules.irmc import raid +from ironic.drivers.modules.irmc import vendor from ironic.drivers.modules import noop from ironic.drivers.modules import pxe @@ -77,3 +78,8 @@ class IRMCHardware(generic.GenericHardware): def supported_raid_interfaces(self): """List of supported raid interfaces.""" return [noop.NoRAID, raid.IRMCRAID, agent.AgentRAID] + + @property + def supported_vendor_interfaces(self): + """List of supported vendor interfaces.""" + return [noop.NoVendor, vendor.IRMCVendorPassthru] diff --git a/ironic/drivers/modules/irmc/common.py b/ironic/drivers/modules/irmc/common.py index 2df85eeb65..1c32fd291c 100644 --- a/ironic/drivers/modules/irmc/common.py +++ b/ironic/drivers/modules/irmc/common.py @@ -15,9 +15,12 @@ """ Common functionalities shared between different iRMC modules. """ +import json import os +import re from oslo_log import log as logging +from oslo_serialization import jsonutils from oslo_utils import importutils from oslo_utils import strutils @@ -31,6 +34,16 @@ scci = importutils.try_import('scciclient.irmc.scci') elcm = importutils.try_import('scciclient.irmc.elcm') LOG = logging.getLogger(__name__) + + +IRMC_OS_NAME_R = re.compile(r'iRMC\s+S\d+') +IRMC_OS_NAME_NUM_R = re.compile(r'\d+$') +IRMC_FW_VER_R = re.compile(r'\d(\.\d+)*\w*') +IRMC_FW_VER_NUM_R = re.compile(r'\d(\.\d+)*') + + +ELCM_STATUS_PATH = '/rest/v1/Oem/eLCM/eLCMStatus' + REQUIRED_PROPERTIES = { 'irmc_address': _("IP address or hostname of the iRMC. Required."), 'irmc_username': _("Username for the iRMC with administrator privileges. " @@ -436,3 +449,202 @@ def set_secure_boot_mode(node, enable): raise exception.IRMCOperationError( operation=_("setting secure boot mode"), error=irmc_exception) + + +def check_elcm_license(node): + """Connect to iRMC and return status of eLCM license + + This function connects to iRMC REST API and check whether eLCM + license is active. This function can be used to check connection to + iRMC REST API. + + :param node: An ironic node object + :returns: dictionary whose keys are 'active' and 'status_code'. + value of 'active' is boolean showing if eLCM license is active + and value of 'status_code' is int which is HTTP return code + from iRMC REST API access + :raises: InvalidParameterValue if invalid value is contained + in the 'driver_info' property. + :raises: MissingParameterValue if some mandatory key is missing + in the 'driver_info' property. + :raises: IRMCOperationError if the operation fails. + """ + try: + d_info = parse_driver_info(node) + # GET to /rest/v1/Oem/eLCM/eLCMStatus returns + # JSON data like this: + # + # { + # "eLCMStatus":{ + # "EnabledAndLicenced":"true", + # "SDCardMounted":"false" + # } + # } + # + # EnabledAndLicenced tells whether eLCM license is valid + # + r = elcm.elcm_request(d_info, 'GET', ELCM_STATUS_PATH) + + # If r.status_code is 200, it means success and r.text is JSON. + # If it is 500, it means there is problem at iRMC side + # and iRMC cannot return eLCM status. + # If it was 401, elcm_request raises SCCIClientError. + # Otherwise, r.text may not be JSON. + if r.status_code == 200: + license_active = strutils.bool_from_string( + jsonutils.loads(r.text)['eLCMStatus']['EnabledAndLicenced'], + strict=True) + else: + license_active = False + + return {'active': license_active, 'status_code': r.status_code} + except (scci.SCCIError, + json.JSONDecodeError, + TypeError, + KeyError, + ValueError) as irmc_exception: + LOG.error("Failed to check eLCM license status for node $(node)s", + {'node': node.uuid}) + raise exception.IRMCOperationError( + operation='checking eLCM license status', + error=irmc_exception) + + +def set_irmc_version(task): + """Fetch and save iRMC firmware version. + + This function should be called before calling any other functions which + need to check node's iRMC firmware version. + + Set `/` to driver_internal_info['irmc_fw_version'] + + :param node: An ironic node object + :raises: InvalidParameterValue if invalid value is contained + in the 'driver_info' property. + :raises: MissingParameterValue if some mandatory key is missing + in the 'driver_info' property. + :raises: IRMCOperationError if the operation fails. + :raises: NodeLocked if the target node is already locked. + """ + + node = task.node + try: + report = get_irmc_report(node) + irmc_os, fw_version = scci.get_irmc_version_str(report) + + fw_ver = node.driver_internal_info.get('irmc_fw_version') + if fw_ver != '/'.join([irmc_os, fw_version]): + task.upgrade_lock(purpose='saving firmware version') + node.set_driver_internal_info('irmc_fw_version', + f"{irmc_os}/{fw_version}") + node.save() + except scci.SCCIError as irmc_exception: + LOG.error("Failed to fetch iRMC FW version for node %s", + node.uuid) + raise exception.IRMCOperationError( + operation=_("fetching irmc fw version "), + error=irmc_exception) + + +def _version_lt(v1, v2): + v1_l = v1.split('.') + v2_l = v2.split('.') + if len(v1_l) <= len(v2_l): + v1_l.extend(['0'] * (len(v2_l) - len(v1_l))) + else: + v2_l.extend(['0'] * (len(v1_l) - len(v2_l))) + + for i in range(len(v1_l)): + if int(v1_l[i]) < int(v2_l[i]): + return True + elif int(v1_l[i]) > int(v2_l[i]): + return False + else: + return False + + +def _version_le(v1, v2): + v1_l = v1.split('.') + v2_l = v2.split('.') + if len(v1_l) <= len(v2_l): + v1_l.extend(['0'] * (len(v2_l) - len(v1_l))) + else: + v2_l.extend(['0'] * (len(v1_l) - len(v2_l))) + + for i in range(len(v1_l)): + if int(v1_l[i]) < int(v2_l[i]): + return True + elif int(v1_l[i]) > int(v2_l[i]): + return False + else: + return True + + +def within_version_ranges(node, version_ranges): + """Read saved iRMC FW version and check if it is within the passed ranges. + + :param node: An ironic node object + :param version_ranges: A Python dictionary containing version ranges in the + next format: : , where is a string representing + iRMC OS number (e.g. '4') and is a dictionaries indicating + the specific firmware version ranges under the iRMC OS number . + + The dictionary used in only has two keys: 'min' and 'upper', + and value of each key is a string representing iRMC firmware version + number or None. Both keys can be absent and their value can be None. + + It is acceptable to not set ranges for a (for example set + to None, {}, etc...), in this case, this function only + checks if the node's iRMC OS number matches the . + + Valid example: + {'3': None, # all version of iRMC S3 matches + '4': {}, # all version of iRMC S4 matches + # all version of iRMC S5 matches + '5': {'min': None, 'upper': None}, + # iRMC S6 whose version is >=1.20 matches + '6': {'min': '1.20', 'upper': None}, + # iRMC S7 whose version is + # 5.51<= (version) <8.23 matches + '7': {'min': '5.51', 'upper': '8.23'}} + + :returns: True if node's iRMC FW is in range, False if not or + fails to parse firmware version + """ + + try: + fw_version = node.driver_internal_info.get('irmc_fw_version', '') + irmc_os, irmc_ver = fw_version.split('/') + + if IRMC_OS_NAME_R.match(irmc_os) and IRMC_FW_VER_R.match(irmc_ver): + os_num = IRMC_OS_NAME_NUM_R.search(irmc_os).group(0) + fw_num = IRMC_FW_VER_NUM_R.search(irmc_ver).group(0) + + if os_num not in version_ranges: + return False + + v_range = version_ranges[os_num] + + # An OS number with no ranges setted means no need to check + # specific version, all the version under this OS number is valid. + if not v_range: + return True + + # Specific range is setted, check if the node's + # firmware version is within it. + min_ver = v_range.get('min') + upper_ver = v_range.get('upper') + flag = True + if min_ver: + flag = _version_le(min_ver, fw_num) + if flag and upper_ver: + flag = _version_lt(fw_num, upper_ver) + return flag + + except Exception: + # All exceptions are ignored + pass + + LOG.warning('Failed to parse iRMC firmware version on node %(uuid)s: ' + '%(fw_ver)s', {'uuid': node.uuid, 'fw_ver': fw_version}) + return False diff --git a/ironic/drivers/modules/irmc/management.py b/ironic/drivers/modules/irmc/management.py index 7f480fd4ba..4fd31eb6c8 100644 --- a/ironic/drivers/modules/irmc/management.py +++ b/ironic/drivers/modules/irmc/management.py @@ -401,3 +401,41 @@ class IRMCManagement(ipmitool.IPMIManagement): not supported by the driver or the hardware """ return irmc_common.set_secure_boot_mode(task.node, state) + + @base.verify_step(priority=10) + def verify_http_https_connection_and_fw_version(self, task): + """Check http(s) connection to iRMC and save fw version + + :param task' A task from TaskManager + 'raises: IRMCOperationError + """ + error_msg_https = ('Access to REST API returns unexpected ' + 'status code. Check driver_info parameter ' + 'related to iRMC driver') + error_msg_http = ('Access to REST API returns unexpected ' + 'status code. Check driver_info parameter ' + 'or version of iRMC because iRMC does not ' + 'support HTTP connection to iRMC REST API ' + 'since iRMC S6 2.00.') + try: + # Check connection to iRMC + elcm_license = irmc_common.check_elcm_license(task.node) + + # On iRMC S6 2.00, access to REST API through HTTP returns 404 + if elcm_license.get('status_code') not in (200, 500): + port = task.node.driver_info.get( + 'irmc_port', CONF.irmc.get('port')) + if port == 80: + e_msg = error_msg_http + else: + e_msg = error_msg_https + raise exception.IRMCOperationError( + operation='establishing connection to REST API', + error=e_msg) + + irmc_common.set_irmc_version(task) + except (exception.InvalidParameterValue, + exception.MissingParameterValue) as irmc_exception: + raise exception.IRMCOperationError( + operation='configuration validation', + error=irmc_exception) diff --git a/ironic/drivers/modules/irmc/vendor.py b/ironic/drivers/modules/irmc/vendor.py new file mode 100644 index 0000000000..35535f69d6 --- /dev/null +++ b/ironic/drivers/modules/irmc/vendor.py @@ -0,0 +1,75 @@ +# Copyright 2022 FUJITSU LIMITED +# +# 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. + +""" +Vendor interface of iRMC driver +""" + +from ironic.common import exception +from ironic.common.i18n import _ +from ironic.drivers import base +from ironic.drivers.modules.irmc import common as irmc_common + + +class IRMCVendorPassthru(base.VendorInterface): + def get_properties(self): + """Return the properties of the interface. + + :returns: Dictionary of : entries. + """ + return irmc_common.COMMON_PROPERTIES + + def validate(self, task, method=None, **kwargs): + """Validate vendor-specific actions. + + This method validates whether the 'driver_info' property of the + supplied node contains the required information for this driver. + + :param task: An instance of TaskManager. + :param method: Name of vendor passthru method + :raises: InvalidParameterValue if invalid value is contained + in the 'driver_info' property. + :raises: MissingParameterValue if some mandatory key is missing + in the 'driver_info' property. + """ + irmc_common.parse_driver_info(task.node) + + @base.passthru(['POST'], + async_call=True, + description='Connect to iRMC and fetch iRMC firmware ' + 'version and, if firmware version has not been cached ' + 'in or actual firmware version is different from one in ' + 'driver_internal_info/irmc_fw_version, store firmware ' + 'version in driver_internal_info/irmc_fw_version.', + attach=False, + require_exclusive_lock=False) + def cache_irmc_firmware_version(self, task, **kwargs): + """Fetch and save iRMC firmware version. + + This method connects to iRMC and fetch iRMC firmware verison. + If fetched firmware version is not cached in or is different from + one in driver_internal_info/irmc_fw_version, store fetched version + in driver_internal_info/irmc_fw_version. + + :param task: An instance of TaskManager. + :raises: IRMCOperationError if some error occurs + """ + try: + irmc_common.set_irmc_version(task) + except (exception.IRMCOperationError, + exception.InvalidParameterValue, + exception.MissingParameterValue, + exception.NodeLocked) as e: + raise exception.IRMCOperationError( + operation=_('caching firmware version'), error=e) diff --git a/ironic/tests/unit/drivers/modules/irmc/test_common.py b/ironic/tests/unit/drivers/modules/irmc/test_common.py index 9dbb380baf..f125d7bd5a 100644 --- a/ironic/tests/unit/drivers/modules/irmc/test_common.py +++ b/ironic/tests/unit/drivers/modules/irmc/test_common.py @@ -412,3 +412,132 @@ class IRMCCommonMethodsTestCase(BaseIRMCTest): info = irmc_common.parse_driver_info(task.node) mock_elcm.set_secure_boot_mode.assert_called_once_with( info, True) + + @mock.patch.object(irmc_common, 'elcm', + spec_set=mock_specs.SCCICLIENT_IRMC_ELCM_SPEC) + def test_check_elcm_license_success_with_200(self, elcm_mock): + elcm_req_mock = elcm_mock.elcm_request + json_data = ('{ "eLCMStatus" : { "EnabledAndLicenced" : "true" , ' + '"SDCardMounted" : "false" } }') + func_return_value = {'active': True, 'status_code': 200} + response_mock = elcm_req_mock.return_value + response_mock.status_code = 200 + response_mock.text = json_data + self.assertEqual(irmc_common.check_elcm_license(self.node), + func_return_value) + + @mock.patch.object(irmc_common, 'elcm', + spec_set=mock_specs.SCCICLIENT_IRMC_ELCM_SPEC) + def test_check_elcm_license_success_with_500(self, elcm_mock): + elcm_req_mock = elcm_mock.elcm_request + json_data = '' + func_return_value = {'active': False, 'status_code': 500} + response_mock = elcm_req_mock.return_value + response_mock.status_code = 500 + response_mock.text = json_data + self.assertEqual(irmc_common.check_elcm_license(self.node), + func_return_value) + + @mock.patch.object(irmc_common, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + @mock.patch.object(irmc_common, 'elcm', + spec_set=mock_specs.SCCICLIENT_IRMC_ELCM_SPEC) + def test_check_elcm_license_fail_invalid_json(self, elcm_mock, scci_mock): + scci_mock.SCCIError = Exception + elcm_req_mock = elcm_mock.elcm_request + json_data = '' + response_mock = elcm_req_mock.return_value + response_mock.status_code = 200 + response_mock.text = json_data + self.assertRaises(exception.IRMCOperationError, + irmc_common.check_elcm_license, self.node) + + @mock.patch.object(irmc_common, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + @mock.patch.object(irmc_common, 'elcm', + spec_set=mock_specs.SCCICLIENT_IRMC_ELCM_SPEC) + def test_check_elcm_license_fail_elcm_error(self, elcm_mock, scci_mock): + scci_mock.SCCIError = Exception + elcm_req_mock = elcm_mock.elcm_request + elcm_req_mock.side_effect = scci_mock.SCCIError + self.assertRaises(exception.IRMCOperationError, + irmc_common.check_elcm_license, self.node) + + @mock.patch.object(irmc_common, 'get_irmc_report', autospec=True) + @mock.patch.object(irmc_common, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + def test_set_irmc_version_success(self, scci_mock, get_report_mock): + version_str = 'iRMC S6/2.00' + scci_mock.get_irmc_version_str.return_value = version_str.split('/') + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + irmc_common.set_irmc_version(task) + self.assertEqual(version_str, + task.node.driver_internal_info['irmc_fw_version']) + + @mock.patch.object(irmc_common, 'get_irmc_report', autospec=True) + @mock.patch.object(irmc_common, 'scci', + spec_set=mock_specs.SCCICLIENT_IRMC_SCCI_SPEC) + def test_set_irmc_version_fail(self, scci_mock, get_report_mock): + scci_mock.SCCIError = Exception + get_report_mock.side_effect = scci_mock.SCCIError + with task_manager.acquire(self.context, self.node.uuid, + shared=False) as task: + self.assertRaises(exception.IRMCOperationError, + irmc_common.set_irmc_version, task) + + def test_within_version_ranges_success(self): + self.node.set_driver_internal_info('irmc_fw_version', 'iRMC S6/2.00') + ver_range_list = [ + {'4': {'upper': '1.05'}, + '6': {'min': '1.95', 'upper': '2.01'} + }, + {'4': {'upper': '1.05'}, + '6': {'min': '1.95', 'upper': None} + }, + {'4': {'upper': '1.05'}, + '6': {'min': '1.95'} + }, + {'4': {'upper': '1.05'}, + '6': {} + }, + {'4': {'upper': '1.05'}, + '6': None + }] + for range_dict in ver_range_list: + with self.subTest(): + self.assertTrue(irmc_common.within_version_ranges(self.node, + range_dict)) + + def test_within_version_ranges_success_out_range(self): + self.node.set_driver_internal_info('irmc_fw_version', 'iRMC S6/2.00') + ver_range_list = [ + {'4': {'upper': '1.05'}, + '6': {'min': '1.95', 'upper': '2.00'} + }, + {'4': {'upper': '1.05'}, + '6': {'min': '1.95', 'upper': '1.99'} + }, + {'4': {'upper': '1.05'}, + }] + for range_dict in ver_range_list: + with self.subTest(): + self.assertFalse(irmc_common.within_version_ranges(self.node, + range_dict)) + + def test_within_version_ranges_fail_no_match(self): + self.node.set_driver_internal_info('irmc_fw_version', 'ver/2.00') + ver_range = { + '4': {'upper': '1.05'}, + '6': {'min': '1.95', 'upper': '2.01'} + } + self.assertFalse(irmc_common.within_version_ranges(self.node, + ver_range)) + + def test_within_version_ranges_fail_no_version_set(self): + ver_range = { + '4': {'upper': '1.05'}, + '6': {'min': '1.95', 'upper': '2.01'} + } + self.assertFalse(irmc_common.within_version_ranges(self.node, + ver_range)) diff --git a/ironic/tests/unit/drivers/modules/irmc/test_management.py b/ironic/tests/unit/drivers/modules/irmc/test_management.py index b2ab5afce0..878c7d2cba 100644 --- a/ironic/tests/unit/drivers/modules/irmc/test_management.py +++ b/ironic/tests/unit/drivers/modules/irmc/test_management.py @@ -500,3 +500,93 @@ class IRMCManagementTestCase(test_common.BaseIRMCTest): result = task.driver.management.restore_irmc_bios_config(task) self.assertIsNone(result) mock_restore_bios.assert_called_once_with(task) + + @mock.patch.object(irmc_common, 'set_irmc_version', autospec=True) + @mock.patch.object(irmc_common, 'check_elcm_license', autospec=True) + def test_verify_http_s_connection_and_fw_ver_success(self, + check_elcm_mock, + set_irmc_ver_mock): + check_elcm_mock.return_value = {'active': True, + 'status_code': 200} + with task_manager.acquire(self.context, self.node.uuid) as task: + irmc_mng = irmc_management.IRMCManagement() + irmc_mng.verify_http_https_connection_and_fw_version(task) + check_elcm_mock.assert_called_with(task.node) + set_irmc_ver_mock.assert_called_with(task) + + @mock.patch.object(irmc_common, 'set_irmc_version', autospec=True) + @mock.patch.object(irmc_common, 'check_elcm_license', autospec=True) + def test_verify_http_s_connection_and_fw_ver_raise_http_success( + self, check_elcm_mock, set_irmc_ver_mock): + error_msg_http = ('iRMC establishing connection to REST API ' + 'failed. Reason: ' + 'Access to REST API returns unexpected ' + 'status code. Check driver_info parameter ' + 'or version of iRMC because iRMC does not ' + 'support HTTP connection to iRMC REST API ' + 'since iRMC S6 2.00.') + + check_elcm_mock.return_value = {'active': False, + 'status_code': 404} + + with task_manager.acquire(self.context, self.node.uuid) as task: + irmc_mng = irmc_management.IRMCManagement() + + task.node.driver_info['irmc_port'] = 80 + self.assertRaisesRegex( + exception.IRMCOperationError, + error_msg_http, + irmc_mng.verify_http_https_connection_and_fw_version, + task) + check_elcm_mock.assert_called_with(task.node) + set_irmc_ver_mock.assert_not_called() + + @mock.patch.object(irmc_common, 'set_irmc_version', autospec=True) + @mock.patch.object(irmc_common, 'check_elcm_license', autospec=True) + def test_verify_http_s_connection_and_fw_ver_raise_https_success( + self, check_elcm_mock, set_irmc_ver_mock): + error_msg_https = ('iRMC establishing connection to REST API ' + 'failed. Reason: ' + 'Access to REST API returns unexpected ' + 'status code. Check driver_info parameter ' + 'related to iRMC driver') + + check_elcm_mock.return_value = {'active': False, + 'status_code': 404} + + with task_manager.acquire(self.context, self.node.uuid) as task: + irmc_mng = irmc_management.IRMCManagement() + task.node.driver_info['irmc_port'] = 443 + self.assertRaisesRegex( + exception.IRMCOperationError, + error_msg_https, + irmc_mng.verify_http_https_connection_and_fw_version, + task) + check_elcm_mock.assert_called_with(task.node) + set_irmc_ver_mock.assert_not_called() + + @mock.patch.object(irmc_common, 'set_irmc_version', autospec=True) + @mock.patch.object(irmc_common, 'check_elcm_license', autospec=True) + def test_verify_http_s_connection_and_fw_ver_fail_invalid( + self, check_elcm_mock, set_irmc_ver_mock): + check_elcm_mock.side_effect = exception.InvalidParameterValue + with task_manager.acquire(self.context, self.node.uuid) as task: + irmc_mng = irmc_management.IRMCManagement() + self.assertRaises( + exception.IRMCOperationError, + irmc_mng.verify_http_https_connection_and_fw_version, + task) + check_elcm_mock.assert_called_with(task.node) + + @mock.patch.object(irmc_common, 'set_irmc_version', autospec=True) + @mock.patch.object(irmc_common, 'check_elcm_license', autospec=True) + def test_verify_http_s_connection_and_fw_ver_fail_missing( + self, check_elcm_mock, set_irmc_ver_mock): + check_elcm_mock.side_effect = exception.MissingParameterValue + with task_manager.acquire(self.context, self.node.uuid) as task: + irmc_mng = irmc_management.IRMCManagement() + self.assertRaises( + exception.IRMCOperationError, + irmc_mng.verify_http_https_connection_and_fw_version, + task) + check_elcm_mock.assert_called_with(task.node) diff --git a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py index b58504fbec..78939c91af 100644 --- a/ironic/tests/unit/drivers/third_party_driver_mock_specs.py +++ b/ironic/tests/unit/drivers/third_party_driver_mock_specs.py @@ -95,9 +95,11 @@ SCCICLIENT_IRMC_SCCI_SPEC = ( 'get_virtual_fd_set_params_cmd', 'get_essential_properties', 'get_capabilities_properties', + 'get_irmc_version_str', ) SCCICLIENT_IRMC_ELCM_SPEC = ( 'backup_bios_config', + 'elcm_request', 'restore_bios_config', 'set_secure_boot_mode', ) diff --git a/releasenotes/notes/fix-irmc-s6-2.00-http-incompatibility-61a31d12aa33fbd8.yaml b/releasenotes/notes/fix-irmc-s6-2.00-http-incompatibility-61a31d12aa33fbd8.yaml new file mode 100644 index 0000000000..f6e91c1abc --- /dev/null +++ b/releasenotes/notes/fix-irmc-s6-2.00-http-incompatibility-61a31d12aa33fbd8.yaml @@ -0,0 +1,19 @@ +--- +upgrade: + - | + Since iRMC versions S6 2.00 and later, iRMC firmware doesn't + support HTTP connection to REST API. Operators need to set + ``[irmc] port`` in ironic.conf or ``driver_info/irmc_port`` + to 443. +features: + - | + Adds verify step and node vendor passthru method to deal with + a firmware incompatibility issue with iRMC versions S6 2.00 + and later in which HTTP connection to REST API is not supported + and HTTPS connections to REST API is required. + + Verify step checks connection to iRMC REST API and if connection + succeeds, it fetches version of iRMC firmware and store it in + ``driver_internal_info/irmc_fw_version``. Ironic operators use + node vendor passthru method to fetch & update iRMC firmware + version cached in ``driver_internal_info/irmc_fw_version``. diff --git a/setup.cfg b/setup.cfg index 8354ae8ccb..915d50ccce 100644 --- a/setup.cfg +++ b/setup.cfg @@ -168,6 +168,7 @@ ironic.hardware.interfaces.vendor = idrac-wsman = ironic.drivers.modules.drac.vendor_passthru:DracWSManVendorPassthru idrac-redfish = ironic.drivers.modules.drac.vendor_passthru:DracRedfishVendorPassthru ilo = ironic.drivers.modules.ilo.vendor:VendorPassthru + irmc = ironic.drivers.modules.irmc.vendor:IRMCVendorPassthru ipmitool = ironic.drivers.modules.ipmitool:VendorPassthru no-vendor = ironic.drivers.modules.noop:NoVendor redfish = ironic.drivers.modules.redfish.vendor:RedfishVendorPassthru