Merge "[iRMC] identify BMC firmware version"
This commit is contained in:
commit
8cb5ba9ff8
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
75
ironic/drivers/modules/irmc/vendor.py
Normal file
75
ironic/drivers/modules/irmc/vendor.py
Normal file
@ -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)
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
)
|
||||
|
@ -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``.
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user