summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2023-01-18 16:32:31 +0000
committerGerrit Code Review <review@openstack.org>2023-01-18 16:32:31 +0000
commit8cb5ba9ff817be12de6468a4b9d512bac07b3025 (patch)
tree5780b14f488f1eeadf457b3a6ddd81f4e7ce4623
parenta48af6b5f13598ef83fff6dfd5a01480ed23743d (diff)
parenteae33a0acbfdbe30b5d79360e76668737866e371 (diff)
downloadironic-8cb5ba9ff817be12de6468a4b9d512bac07b3025.tar.gz
Merge "[iRMC] identify BMC firmware version"
-rw-r--r--doc/source/admin/drivers/irmc.rst19
-rw-r--r--ironic/drivers/irmc.py6
-rw-r--r--ironic/drivers/modules/irmc/common.py212
-rw-r--r--ironic/drivers/modules/irmc/management.py38
-rw-r--r--ironic/drivers/modules/irmc/vendor.py75
-rw-r--r--ironic/tests/unit/drivers/modules/irmc/test_common.py129
-rw-r--r--ironic/tests/unit/drivers/modules/irmc/test_management.py90
-rw-r--r--ironic/tests/unit/drivers/third_party_driver_mock_specs.py2
-rw-r--r--releasenotes/notes/fix-irmc-s6-2.00-http-incompatibility-61a31d12aa33fbd8.yaml19
-rw-r--r--setup.cfg1
10 files changed, 591 insertions, 0 deletions
diff --git a/doc/source/admin/drivers/irmc.rst b/doc/source/admin/drivers/irmc.rst
index 83d7eccb3..dfb61e227 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 50bb9114d..06408359b 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 2df85eeb6..1c32fd291 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 `<iRMC OS>/<fw version>` 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: <os_n>: <ranges>, where <os_n> is a string representing
+ iRMC OS number (e.g. '4') and <ranges> is a dictionaries indicating
+ the specific firmware version ranges under the iRMC OS number <os_n>.
+
+ The dictionary used in <ranges> 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 <os_n> (for example set
+ <ranges> to None, {}, etc...), in this case, this function only
+ checks if the node's iRMC OS number matches the <os_n>.
+
+ Valid <version_ranges> 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 7f480fd4b..4fd31eb6c 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 000000000..35535f69d
--- /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 <property name>:<property description> 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 9dbb380ba..f125d7bd5 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 b2ab5afce..878c7d2cb 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 b58504fbe..78939c91a 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 000000000..f6e91c1ab
--- /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 8354ae8cc..915d50ccc 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