[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
This commit is contained in:
Vanou Ishii 2022-11-08 21:18:13 -05:00
parent 4d66609e95
commit eae33a0acb
10 changed files with 591 additions and 0 deletions

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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)

View 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)

View File

@ -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))

View File

@ -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)

View File

@ -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',
)

View File

@ -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``.

View File

@ -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