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