summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShukun Song <song.shukun@jp.fujitsu.com>2022-06-10 20:09:50 +0900
committerShukun Song <song.shukun@jp.fujitsu.com>2022-11-15 18:17:37 +0900
commitef0e33edf2b0420dc7fe851e41128ff86dc8ffed (patch)
treef823297319351df97e73052245bcdfe8f17b036e
parenta26509b7ae942aa8377373f458129ac313f4d82f (diff)
downloadironic-ef0e33edf2b0420dc7fe851e41128ff86dc8ffed.tar.gz
Add SNMPv3 authentication functionality
Currently when using SNMPv3, iRMC driver does not use SNMPv3 authentication parameters so the SNMPv3 authentication will always fail. And iRMC cannot recognize FIPS mode, so when FIPS mode is enabled, iRMC driver could still use non-FIPS-compliant algorithms. This commit changes iRMC driver to require and use SNMPv3 authentication parameters when 'irmc_snmp_version' is set to v3 and also makes iRMC driver to force 'irmc_snmp_version' to v3, 'irmc_snmp_auth_proto' to SHA and 'irmc_snmp_priv_proto' to AES when FIPS mode is enabled, because currently among the algorithms supported by iRMC, only SHA and AES are FIPS compliant. Conflicts: ironic/common/utils.py Change-Id: Id6f8996e4d103f849325f54fe0619b4acb43453a Story: 2010085 Task: 45590 (cherry picked from commit 79f82c0262c84f1e317991052d065259b3a4683d) (cherry picked from commit c274231bf5bb9260d2b13865ab20f39131d30b4b)
-rw-r--r--doc/source/admin/drivers/irmc.rst34
-rw-r--r--ironic/common/utils.py12
-rw-r--r--ironic/conf/irmc.py17
-rw-r--r--ironic/drivers/modules/irmc/common.py232
-rw-r--r--ironic/drivers/modules/irmc/inspect.py15
-rw-r--r--ironic/drivers/modules/irmc/power.py15
-rw-r--r--ironic/tests/unit/common/test_utils.py15
-rw-r--r--ironic/tests/unit/drivers/modules/irmc/test_boot.py5
-rw-r--r--ironic/tests/unit/drivers/modules/irmc/test_common.py238
-rw-r--r--releasenotes/notes/irmc-add-snmpv3-security-fca05bfc30f50d1a.yaml28
10 files changed, 551 insertions, 60 deletions
diff --git a/doc/source/admin/drivers/irmc.rst b/doc/source/admin/drivers/irmc.rst
index c92c73679..0a9c3a1ea 100644
--- a/doc/source/admin/drivers/irmc.rst
+++ b/doc/source/admin/drivers/irmc.rst
@@ -181,6 +181,25 @@ Configuration via ``driver_info``
``irmc_deploy_iso`` and ``irmc_boot_iso`` accordingly before the Xena
release.
+* The following properties are also required if ``irmc`` inspect interface is
+ enabled and SNMPv3 inspection is desired.
+
+ - ``driver_info/irmc_snmp_user`` property to be the SNMPv3 username. SNMPv3
+ functionality should be enabled for this user on iRMC server side.
+ - ``driver_info/irmc_snmp_auth_password`` property to be the auth protocol
+ pass phrase. The length of pass phrase should be at least 8 characters.
+ - ``driver_info/irmc_snmp_priv_password`` property to be the privacy protocol
+ pass phrase. The length of pass phrase should be at least 8 characters.
+
+ .. note::
+ When using SNMPv3, python-scciclient in old version (before 0.11.3) can
+ only interact with iRMC with no authentication protocol setted. This means
+ the passwords and protocol settings of the snmp user in iRMC side should
+ all be blank, otherwise python-scciclient will encounter an communication
+ error. If you are using such old version python-scciclient, the
+ ``irmc_snmp_auth_password`` and ``irmc_snmp_priv_password`` properties
+ will be ignored. If you want to set passwords, please update
+ python-scciclient to some newer version (>= 0.11.3).
Configuration via ``ironic.conf``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -220,6 +239,17 @@ Configuration via ``ironic.conf``
and ``v2c``. The default value is ``public``. Optional.
- ``snmp_security``: SNMP security name required for version ``v3``.
Optional.
+ - ``snmp_auth_proto``: The SNMPv3 auth protocol. The valid value and the
+ default value are both ``sha``. We will add more supported valid values
+ in the future. Optional.
+ - ``snmp_priv_proto``: The SNMPv3 privacy protocol. The valid value and
+ the default value are both ``aes``. We will add more supported valid values
+ in the future. Optional.
+
+ .. note::
+ ``snmp_security`` will be ignored if ``driver_info/irmc_snmp_user`` is
+ set. ``snmp_auth_proto`` and ``snmp_priv_proto`` will be ignored if the
+ version of python-scciclient is before 0.11.3.
Override ``ironic.conf`` configuration via ``driver_info``
@@ -237,6 +267,10 @@ Override ``ironic.conf`` configuration via ``driver_info``
- ``driver_info/irmc_snmp_port`` property overrides ``snmp_port``.
- ``driver_info/irmc_snmp_community`` property overrides ``snmp_community``.
- ``driver_info/irmc_snmp_security`` property overrides ``snmp_security``.
+ - ``driver_info/irmc_snmp_auth_proto`` property overrides
+ ``snmp_auth_proto``.
+ - ``driver_info/irmc_snmp_priv_proto`` property overrides
+ ``snmp_priv_proto``.
Optional functionalities for the ``irmc`` hardware type
diff --git a/ironic/common/utils.py b/ironic/common/utils.py
index e15083396..80865a90b 100644
--- a/ironic/common/utils.py
+++ b/ironic/common/utils.py
@@ -654,3 +654,15 @@ def remove_large_keys(var):
return var.__class__(map(remove_large_keys, var))
else:
return var
+
+
+def is_fips_enabled():
+ """Check if FIPS mode is enabled in the system."""
+ try:
+ with open('/proc/sys/crypto/fips_enabled', 'r') as f:
+ content = f.read()
+ if content == "1\n":
+ return True
+ except Exception:
+ pass
+ return False
diff --git a/ironic/conf/irmc.py b/ironic/conf/irmc.py
index 839c9c27e..f417ae2db 100644
--- a/ironic/conf/irmc.py
+++ b/ironic/conf/irmc.py
@@ -73,10 +73,25 @@ opts = [
default='public',
help=_('SNMP community. Required for versions "v1" and "v2c"')),
cfg.StrOpt('snmp_security',
- help=_('SNMP security name. Required for version "v3"')),
+ help=_("SNMP security name. Required for version 'v3'. Will be "
+ "ignored if driver_info/irmc_snmp_user is set.")),
cfg.IntOpt('snmp_polling_interval',
default=10,
help='SNMP polling interval in seconds'),
+ cfg.StrOpt('snmp_auth_proto',
+ default='sha',
+ choices=[('sha', _('Secure Hash Algorithm 1'))],
+ help=_("SNMPv3 message authentication protocol ID. "
+ "Required for version 'v3'. Will be ignored if the "
+ "version of python-scciclient is before 0.11.3. 'sha' "
+ "is supported.")),
+ cfg.StrOpt('snmp_priv_proto',
+ default='aes',
+ choices=[('aes', _('Advanced Encryption Standard'))],
+ help=_("SNMPv3 message privacy (encryption) protocol ID. "
+ "Required for version 'v3'. Will be ignored if the "
+ "version of python-scciclient is before 0.11.3. "
+ "'aes' is supported.")),
cfg.IntOpt('clean_priority_restore_irmc_bios_config',
default=0,
help=_('Priority for restore_irmc_bios_config clean step.')),
diff --git a/ironic/drivers/modules/irmc/common.py b/ironic/drivers/modules/irmc/common.py
index cf3076f72..85a038692 100644
--- a/ironic/drivers/modules/irmc/common.py
+++ b/ironic/drivers/modules/irmc/common.py
@@ -26,6 +26,7 @@ from ironic.common.i18n import _
from ironic.common import utils
from ironic.conf import CONF
import ironic.drivers.modules.irmc.packaging_version as version
+from ironic.drivers.modules import snmp
scci = importutils.try_import('scciclient.irmc.scci')
elcm = importutils.try_import('scciclient.irmc.elcm')
@@ -49,15 +50,8 @@ OPTIONAL_PROPERTIES = {
'irmc_sensor_method': _("Sensor data retrieval method; either "
"'ipmitool' or 'scci'. The default value is "
"'ipmitool'. Optional."),
- 'irmc_snmp_version': _("SNMP protocol version; either 'v1', 'v2c', or "
- "'v3'. The default value is 'v2c'. Optional."),
- 'irmc_snmp_port': _("SNMP port. The default is 161. Optional."),
- 'irmc_snmp_community': _("SNMP community required for versions 'v1' and "
- "'v2c'. The default value is 'public'. "
- "Optional."),
- 'irmc_snmp_security': _("SNMP security name required for version 'v3'. "
- "Optional."),
}
+
OPTIONAL_DRIVER_INFO_PROPERTIES = {
'irmc_verify_ca': _('Either a Boolean value, a path to a CA_BUNDLE '
'file or directory with certificates of trusted '
@@ -69,23 +63,68 @@ OPTIONAL_DRIVER_INFO_PROPERTIES = {
'directory. Defaults to True. Optional'),
}
+SNMP_PROPERTIES = {
+ 'irmc_snmp_version': _("SNMP protocol version; either 'v1', 'v2c', or "
+ "'v3'. The default value is 'v2c'. Optional."),
+ 'irmc_snmp_port': _("SNMP port. The default is 161. Optional."),
+ 'irmc_snmp_community': _("SNMP community required for versions 'v1' and "
+ "'v2c'. The default value is 'public'. "
+ "Optional."),
+ 'irmc_snmp_security': _("SNMP security name required for version 'v3'. "
+ "Optional."),
+}
+
+SNMP_V3_REQUIRED_PROPERTIES = {
+ 'irmc_snmp_user': _("SNMPv3 User-based Security Model (USM) username. "
+ "Required for version 'v3’. "),
+ 'irmc_snmp_auth_password': _("SNMPv3 message authentication key. Must be "
+ "8+ characters long. Required when message "
+ "authentication is used. Will be ignored if "
+ "the version of python-scciclient is before "
+ "0.11.3."),
+ 'irmc_snmp_priv_password': _("SNMPv3 message privacy key. Must be 8+ "
+ "characters long. Required when message "
+ "privacy is used. Will be ignored if the "
+ "version of python-scciclient is before "
+ "0.11.3."),
+}
+
+SNMP_V3_OPTIONAL_PROPERTIES = {
+ 'irmc_snmp_auth_proto': _("SNMPv3 message authentication protocol ID. "
+ "Required for version 'v3'. Will be ignored if "
+ "the version of python-scciclient is before "
+ "0.11.3. 'sha' is supported."),
+ 'irmc_snmp_priv_proto': _("SNMPv3 message privacy (encryption) protocol "
+ "ID. Required for version 'v3'. Will be ignored "
+ "if the version of python-scciclient is before "
+ "0.11.3. 'aes' is supported."),
+}
+
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
COMMON_PROPERTIES.update(OPTIONAL_DRIVER_INFO_PROPERTIES)
+COMMON_PROPERTIES.update(SNMP_PROPERTIES)
+COMMON_PROPERTIES.update(SNMP_V3_REQUIRED_PROPERTIES)
+COMMON_PROPERTIES.update(SNMP_V3_OPTIONAL_PROPERTIES)
SCCI_CERTIFICATION_SUPPORT_VERSION_RANGES = [
- {'min': '0.8.2', 'upp': '0.9.0'},
- {'min': '0.9.4', 'upp': '0.10.0'},
- {'min': '0.10.1', 'upp': '0.11.0'},
- {'min': '0.11.3', 'upp': '0.12.0'},
- {'min': '0.12.0', 'upp': '0.13.0'}]
+ {'min': '0.8.2', 'max': '0.9.0'},
+ {'min': '0.9.4', 'max': '0.10.0'},
+ {'min': '0.10.1', 'max': '0.11.0'},
+ {'min': '0.11.3', 'max': '0.12.0'},
+ {'min': '0.12.0', 'max': '0.13.0'}]
+
+SCCI_SNMPv3_AUTHENTICATION_SUPPORT_VERSION_RANGES = [
+ {'min': '0.10.1', 'max': '0.11.0'},
+ {'min': '0.11.3', 'max': '0.12.0'},
+ {'min': '0.12.2', 'max': '0.13.0'}]
-def scci_support_certification():
+def _scci_version_in(version_ranges):
scciclient_version = version.parse(scci_mod.__version__)
- for rangev in SCCI_CERTIFICATION_SUPPORT_VERSION_RANGES:
+ for rangev in version_ranges:
if (version.parse(rangev['min']) <= scciclient_version
- < version.parse(rangev['upp'])):
+ < version.parse(rangev['max'])):
return True
return False
@@ -139,29 +178,6 @@ def parse_driver_info(node):
error_msgs.append(
_("Value '%s' is not supported for 'irmc_sensor_method'.") %
d_info['irmc_sensor_method'])
- if d_info['irmc_snmp_version'].lower() not in ('v1', 'v2c', 'v3'):
- error_msgs.append(
- _("Value '%s' is not supported for 'irmc_snmp_version'.") %
- d_info['irmc_snmp_version'])
- if not isinstance(d_info['irmc_snmp_port'], int):
- error_msgs.append(
- _("Value '%s' is not an integer for 'irmc_snmp_port'") %
- d_info['irmc_snmp_port'])
- if (d_info['irmc_snmp_version'].lower() in ('v1', 'v2c')
- and d_info['irmc_snmp_community']
- and not isinstance(d_info['irmc_snmp_community'], str)):
- error_msgs.append(
- _("Value '%s' is not a string for 'irmc_snmp_community'") %
- d_info['irmc_snmp_community'])
- if d_info['irmc_snmp_version'].lower() == 'v3':
- if d_info['irmc_snmp_security']:
- if not isinstance(d_info['irmc_snmp_security'], str):
- error_msgs.append(
- _("Value '%s' is not a string for "
- "'irmc_snmp_security'") % d_info['irmc_snmp_security'])
- else:
- error_msgs.append(
- _("'irmc_snmp_security' has to be set for SNMP version 3."))
verify_ca = d_info.get('irmc_verify_ca')
if verify_ca is None:
@@ -199,9 +215,143 @@ def parse_driver_info(node):
"driver_info:\n%s") % "\n".join(error_msgs))
raise exception.InvalidParameterValue(msg)
+ d_info.update(_parse_snmp_driver_info(node, info))
+
return d_info
+def _parse_snmp_driver_info(node, info):
+ """Parses the SNMP related driver_info parameters.
+
+ :param node: An Ironic node object.
+ :param info: driver_info dictionary.
+ :returns: A dictionary containing SNMP information.
+ :raises: MissingParameterValue if any of the mandatory
+ parameter values are not provided.
+ :raises: InvalidParameterValue if there is any invalid
+ value provided.
+ """
+ snmp_info = {param: info.get(param, CONF.irmc.get(param[len('irmc_'):]))
+ for param in SNMP_PROPERTIES}
+ valid_versions = {"v1": snmp.SNMP_V1,
+ "v2c": snmp.SNMP_V2C,
+ "v3": snmp.SNMP_V3}
+
+ if snmp_info['irmc_snmp_version'].lower() not in valid_versions:
+ raise exception.InvalidParameterValue(_(
+ "Value '%s' is not supported for 'irmc_snmp_version'.") %
+ snmp_info['irmc_snmp_version']
+ )
+ snmp_info["irmc_snmp_version"] = \
+ valid_versions[snmp_info["irmc_snmp_version"].lower()]
+
+ snmp_info['irmc_snmp_port'] = utils.validate_network_port(
+ snmp_info['irmc_snmp_port'], 'irmc_snmp_port')
+
+ if snmp_info['irmc_snmp_version'] != snmp.SNMP_V3:
+ if (snmp_info['irmc_snmp_community']
+ and not isinstance(snmp_info['irmc_snmp_community'], str)):
+ raise exception.InvalidParameterValue(_(
+ "Value '%s' is not a string for 'irmc_snmp_community'") %
+ snmp_info['irmc_snmp_community'])
+ if utils.is_fips_enabled():
+ raise exception.InvalidParameterValue(_(
+ "'v3' has to be set for 'irmc_snmp_version' "
+ "when FIPS mode is enabled."))
+
+ else:
+ # Parse snmp user info
+ if 'irmc_snmp_user' in info:
+ if not isinstance(info['irmc_snmp_user'], str):
+ raise exception.InvalidParameterValue(_(
+ "Value %s is not a string for 'irmc_snmp_user'.") %
+ info['irmc_snmp_user'])
+ snmp_info['irmc_snmp_user'] = info['irmc_snmp_user']
+ if snmp_info['irmc_snmp_security']:
+ LOG.warning(_("'irmc_snmp_security' is ignored in favor of "
+ "'irmc_snmp_user'. Please remove "
+ "'irmc_snmp_security' from node %s "
+ "configuration."), node.uuid)
+ else:
+ if not snmp_info['irmc_snmp_security']:
+ raise exception.MissingParameterValue(_(
+ "'irmc_snmp_user' should be set when using SNMPv3."))
+ if not isinstance(snmp_info['irmc_snmp_security'], str):
+ raise exception.InvalidParameterValue(_(
+ "Value %s is not a string for 'irmc_snmp_security'.") %
+ snmp_info['irmc_snmp_security'])
+ snmp_info['irmc_snmp_user'] = snmp_info['irmc_snmp_security']
+
+ if _scci_version_in(SCCI_SNMPv3_AUTHENTICATION_SUPPORT_VERSION_RANGES):
+ snmp_info.update(_parse_snmp_v3_crypto_info(info))
+ else:
+ # For compatible with old version of python-scciclient
+ snmp_info['irmc_snmp_security'] = snmp_info['irmc_snmp_user']
+ if 'irmc_snmp_auth_password' in info or \
+ 'irmc_snmp_priv_password' in info:
+ LOG.warning(_("'irmc_snmp_auth_password' and "
+ "'irmc_snmp_priv_password' in node %(node)s "
+ "configuration are ignored. "
+ "Python-scciclient version %(version)s can only "
+ "communicate with iRMC with no authentication "
+ "protocol setted. This means the authentication "
+ "protocol, private protocol and password of the "
+ "server's SNMPv3 user should all be blank, "
+ "otherwise python-scciclient will encounter an "
+ "authentication error. If you want to set "
+ "password, please update python-scciclient to "
+ "a newer version (>=0.11.3, <0.12.0)."),
+ {'node': node.uuid,
+ 'version': scci_mod.__version__})
+
+ return snmp_info
+
+
+def _parse_snmp_v3_crypto_info(info):
+ snmp_info = {}
+ valid_values = {'irmc_snmp_auth_proto': ['sha'],
+ 'irmc_snmp_priv_proto': ['aes']}
+ valid_protocols = {'irmc_snmp_auth_proto': snmp.snmp_auth_protocols,
+ 'irmc_snmp_priv_proto': snmp.snmp_priv_protocols}
+ snmp_keys = {'irmc_snmp_auth_password', 'irmc_snmp_priv_password'}
+
+ for param in snmp_keys:
+ try:
+ snmp_info[param] = info[param]
+ except KeyError:
+ raise exception.MissingParameterValue(_(
+ "%s should be set when using SNMPv3.") % param)
+
+ if not isinstance(snmp_info[param], str):
+ raise exception.InvalidParameterValue(_(
+ "The value of %s is not a string.") % param)
+ if len(snmp_info[param]) < 8:
+ raise exception.InvalidParameterValue(_(
+ "%s is too short. (8+ chars required)") % param)
+
+ for param in SNMP_V3_OPTIONAL_PROPERTIES:
+ value = None
+ try:
+ value = info[param]
+ if value not in valid_values[param]:
+ raise exception.InvalidParameterValue(_(
+ "Invalid value %(value)s given for driver info parameter "
+ "%(param)s, the valid values are %(valid_values)s.") %
+ {'param': param,
+ 'value': value,
+ 'valid_values': valid_values[param]})
+ except KeyError:
+ value = CONF.irmc.get(param[len('irmc_'):])
+ snmp_info[param] = valid_protocols[param].get(value)
+ if not snmp_info[param]:
+ raise exception.InvalidParameterValue(_(
+ "Unknown SNMPv3 protocol %(value)s given for "
+ "driver info parameter %(param)s") % {'param': param,
+ 'value': value})
+
+ return snmp_info
+
+
def get_irmc_client(node):
"""Gets an iRMC SCCI client.
@@ -217,7 +367,7 @@ def get_irmc_client(node):
"""
driver_info = parse_driver_info(node)
- if scci_support_certification():
+ if _scci_version_in(SCCI_CERTIFICATION_SUPPORT_VERSION_RANGES):
scci_client = scci.get_client(
driver_info['irmc_address'],
driver_info['irmc_username'],
@@ -272,7 +422,7 @@ def get_irmc_report(node):
"""
driver_info = parse_driver_info(node)
- if scci_support_certification():
+ if _scci_version_in(SCCI_CERTIFICATION_SUPPORT_VERSION_RANGES):
report = scci.get_report(
driver_info['irmc_address'],
driver_info['irmc_username'],
diff --git a/ironic/drivers/modules/irmc/inspect.py b/ironic/drivers/modules/irmc/inspect.py
index 4204ac95b..d31143ee3 100644
--- a/ironic/drivers/modules/irmc/inspect.py
+++ b/ironic/drivers/modules/irmc/inspect.py
@@ -103,11 +103,16 @@ def _get_mac_addresses(node):
:returns: a list of mac addresses.
"""
d_info = irmc_common.parse_driver_info(node)
- snmp_client = snmp.SNMPClient(d_info['irmc_address'],
- d_info['irmc_snmp_port'],
- d_info['irmc_snmp_version'],
- d_info['irmc_snmp_community'],
- d_info['irmc_snmp_security'])
+ snmp_client = snmp.SNMPClient(
+ address=d_info['irmc_address'],
+ port=d_info['irmc_snmp_port'],
+ version=d_info['irmc_snmp_version'],
+ read_community=d_info['irmc_snmp_community'],
+ user=d_info.get('irmc_snmp_user'),
+ auth_proto=d_info.get('irmc_snmp_auth_proto'),
+ auth_key=d_info.get('irmc_snmp_auth_password'),
+ priv_proto=d_info.get('irmc_snmp_priv_proto'),
+ priv_key=d_info.get('irmc_snmp_priv_password'))
node_classes = snmp_client.get_next(NODE_CLASS_OID)
mac_addresses = [':'.join(['%02x' % x for x in mac])
diff --git a/ironic/drivers/modules/irmc/power.py b/ironic/drivers/modules/irmc/power.py
index fbadffb5e..28041d835 100644
--- a/ironic/drivers/modules/irmc/power.py
+++ b/ironic/drivers/modules/irmc/power.py
@@ -93,11 +93,16 @@ def _wait_power_state(task, target_state, timeout=None):
"""
node = task.node
d_info = irmc_common.parse_driver_info(node)
- snmp_client = snmp.SNMPClient(d_info['irmc_address'],
- d_info['irmc_snmp_port'],
- d_info['irmc_snmp_version'],
- d_info['irmc_snmp_community'],
- d_info['irmc_snmp_security'])
+ snmp_client = snmp.SNMPClient(
+ address=d_info['irmc_address'],
+ port=d_info['irmc_snmp_port'],
+ version=d_info['irmc_snmp_version'],
+ read_community=d_info['irmc_snmp_community'],
+ user=d_info.get('irmc_snmp_user'),
+ auth_proto=d_info.get('irmc_snmp_auth_proto'),
+ auth_key=d_info.get('irmc_snmp_auth_password'),
+ priv_proto=d_info.get('irmc_snmp_priv_proto'),
+ priv_key=d_info.get('irmc_snmp_priv_password'))
interval = CONF.irmc.snmp_polling_interval
retry_timeout_soft = timeout or CONF.conductor.soft_power_off_timeout
diff --git a/ironic/tests/unit/common/test_utils.py b/ironic/tests/unit/common/test_utils.py
index a1b73c033..d5d8b42df 100644
--- a/ironic/tests/unit/common/test_utils.py
+++ b/ironic/tests/unit/common/test_utils.py
@@ -306,6 +306,21 @@ class GenericUtilsTestCase(base.TestCase):
utils.is_valid_no_proxy(no_proxy),
msg="'no_proxy' value should be invalid: {}".format(no_proxy))
+ def test_is_fips_enabled(self):
+ with mock.patch('builtins.open', mock.mock_open(read_data='1\n')) as m:
+ self.assertTrue(utils.is_fips_enabled())
+ m.assert_called_once_with('/proc/sys/crypto/fips_enabled', 'r')
+
+ with mock.patch('builtins.open', mock.mock_open(read_data='0\n')) as m:
+ self.assertFalse(utils.is_fips_enabled())
+ m.assert_called_once_with('/proc/sys/crypto/fips_enabled', 'r')
+
+ mock_open = mock.mock_open()
+ mock_open.side_effect = FileNotFoundError
+ with mock.patch('builtins.open', mock_open) as m:
+ self.assertFalse(utils.is_fips_enabled())
+ m.assert_called_once_with('/proc/sys/crypto/fips_enabled', 'r')
+
class TempFilesTestCase(base.TestCase):
diff --git a/ironic/tests/unit/drivers/modules/irmc/test_boot.py b/ironic/tests/unit/drivers/modules/irmc/test_boot.py
index 54f92967e..a1252621e 100644
--- a/ironic/tests/unit/drivers/modules/irmc/test_boot.py
+++ b/ironic/tests/unit/drivers/modules/irmc/test_boot.py
@@ -41,6 +41,7 @@ from ironic.drivers.modules.irmc import common as irmc_common
from ironic.drivers.modules.irmc import management as irmc_management
from ironic.drivers.modules import pxe
from ironic.drivers.modules import pxe_base
+from ironic.drivers.modules import snmp
from ironic.tests import base
from ironic.tests.unit.db import utils as db_utils
from ironic.tests.unit.drivers.modules.irmc import test_common
@@ -60,10 +61,10 @@ PARSED_IFNO = {
'irmc_client_timeout': 60,
'irmc_snmp_community': 'public',
'irmc_snmp_port': 161,
- 'irmc_snmp_version': 'v2c',
- 'irmc_snmp_security': None,
+ 'irmc_snmp_version': snmp.SNMP_V2C,
'irmc_sensor_method': 'ipmitool',
'irmc_verify_ca': True,
+ 'irmc_snmp_security': None,
}
diff --git a/ironic/tests/unit/drivers/modules/irmc/test_common.py b/ironic/tests/unit/drivers/modules/irmc/test_common.py
index c5c70bf95..74b9dc35c 100644
--- a/ironic/tests/unit/drivers/modules/irmc/test_common.py
+++ b/ironic/tests/unit/drivers/modules/irmc/test_common.py
@@ -23,8 +23,10 @@ from oslo_config import cfg
from oslo_utils import uuidutils
from ironic.common import exception
+from ironic.common import utils
from ironic.conductor import task_manager
from ironic.drivers.modules.irmc import common as irmc_common
+from ironic.drivers.modules import snmp
from ironic.tests.unit.db import base as db_base
from ironic.tests.unit.db import utils as db_utils
from ironic.tests.unit.drivers import third_party_driver_mock_specs \
@@ -55,7 +57,9 @@ class BaseIRMCTest(db_base.DbTestCase):
class IRMCValidateParametersTestCase(BaseIRMCTest):
- def test_parse_driver_info(self):
+ @mock.patch.object(utils, 'is_fips_enabled',
+ return_value=False, autospec=True)
+ def test_parse_driver_info(self, mock_check_fips):
info = irmc_common.parse_driver_info(self.node)
self.assertEqual('1.2.3.4', info['irmc_address'])
@@ -65,13 +69,81 @@ class IRMCValidateParametersTestCase(BaseIRMCTest):
self.assertEqual(80, info['irmc_port'])
self.assertEqual('digest', info['irmc_auth_method'])
self.assertEqual('ipmitool', info['irmc_sensor_method'])
- self.assertEqual('v2c', info['irmc_snmp_version'])
+ self.assertEqual(snmp.SNMP_V2C, info['irmc_snmp_version'])
self.assertEqual(161, info['irmc_snmp_port'])
self.assertEqual('public', info['irmc_snmp_community'])
self.assertFalse(info['irmc_snmp_security'])
self.assertTrue(info['irmc_verify_ca'])
- def test_parse_driver_option_default(self):
+ @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__'])
+ def test_parse_driver_info_snmpv3_support_auth(self, mock_scci_module):
+ self.node.driver_info['irmc_snmp_version'] = 'v3'
+ self.node.driver_info['irmc_snmp_user'] = 'admin0'
+ self.node.driver_info['irmc_snmp_auth_password'] = 'valid_key'
+ self.node.driver_info['irmc_snmp_priv_password'] = 'valid_key'
+
+ scci_version_list = ['0.10.1', '0.11.3', '0.12.2']
+ for ver in scci_version_list:
+ with self.subTest(ver=ver):
+ mock_scci_module.__version__ = ver
+ info = irmc_common.parse_driver_info(self.node)
+
+ self.assertEqual('1.2.3.4', info['irmc_address'])
+ self.assertEqual('admin0', info['irmc_username'])
+ self.assertEqual('fake0', info['irmc_password'])
+ self.assertEqual(60, info['irmc_client_timeout'])
+ self.assertEqual(80, info['irmc_port'])
+ self.assertEqual('digest', info['irmc_auth_method'])
+ self.assertEqual('ipmitool', info['irmc_sensor_method'])
+ self.assertEqual(snmp.SNMP_V3, info['irmc_snmp_version'])
+ self.assertEqual(161, info['irmc_snmp_port'])
+ self.assertEqual('public', info['irmc_snmp_community'])
+ self.assertEqual('admin0', info['irmc_snmp_user'])
+ self.assertEqual(snmp.snmp_auth_protocols['sha'],
+ info['irmc_snmp_auth_proto'])
+ self.assertEqual('valid_key', info['irmc_snmp_auth_password'])
+ self.assertEqual(snmp.snmp_priv_protocols['aes'],
+ info['irmc_snmp_priv_proto'])
+ self.assertEqual('valid_key', info['irmc_snmp_priv_password'])
+
+ @mock.patch.object(irmc_common, 'LOG', autospec=True)
+ @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__'])
+ def test_parse_driver_info_snmpv3_not_support_auth(self, mock_scci_module,
+ mock_LOG):
+ self.node.driver_info['irmc_snmp_version'] = 'v3'
+ self.node.driver_info['irmc_snmp_user'] = 'admin0'
+ self.node.driver_info['irmc_snmp_auth_password'] = 'valid_key'
+ self.node.driver_info['irmc_snmp_priv_password'] = 'valid_key'
+
+ scci_version_list = ['0.10.0', '0.11.0', '0.11.2',
+ '0.12.0', '0.12.1', '0.13.0']
+ for ver in scci_version_list:
+ with self.subTest(ver=ver):
+ mock_scci_module.__version__ = ver
+ info = irmc_common.parse_driver_info(self.node)
+
+ self.assertEqual('1.2.3.4', info['irmc_address'])
+ self.assertEqual('admin0', info['irmc_username'])
+ self.assertEqual('fake0', info['irmc_password'])
+ self.assertEqual(60, info['irmc_client_timeout'])
+ self.assertEqual(80, info['irmc_port'])
+ self.assertEqual('digest', info['irmc_auth_method'])
+ self.assertEqual('ipmitool', info['irmc_sensor_method'])
+ self.assertEqual(snmp.SNMP_V3, info['irmc_snmp_version'])
+ self.assertEqual(161, info['irmc_snmp_port'])
+ self.assertEqual('public', info['irmc_snmp_community'])
+ self.assertEqual('admin0', info['irmc_snmp_user'])
+ self.assertEqual('admin0', info['irmc_snmp_security'])
+ self.assertNotIn('irmc_snmp_auth_proto', info)
+ self.assertNotIn('irmc_snmp_auth_password', info)
+ self.assertNotIn('irmc_snmp_priv_proto', info)
+ self.assertNotIn('irmc_snmp_priv_password', info)
+ mock_LOG.warning.assert_called_once()
+ mock_LOG.warning.reset_mock()
+
+ @mock.patch.object(utils, 'is_fips_enabled',
+ return_value=False, autospec=True)
+ def test_parse_driver_option_default(self, mock_check_fips):
self.node.driver_info = {
"irmc_address": "1.2.3.4",
"irmc_username": "admin0",
@@ -133,8 +205,16 @@ class IRMCValidateParametersTestCase(BaseIRMCTest):
self.assertRaises(exception.InvalidParameterValue,
irmc_common.parse_driver_info, self.node)
+ @mock.patch.object(utils, 'is_fips_enabled',
+ return_value=True, autospec=True)
+ def test_parse_driver_info_invalid_snmp_version_fips(self,
+ mock_check_fips):
+ self.assertRaises(exception.InvalidParameterValue,
+ irmc_common.parse_driver_info, self.node)
+ self.assertEqual(1, mock_check_fips.call_count)
+
def test_parse_driver_info_invalid_snmp_port(self):
- self.node.driver_info['irmc_snmp_port'] = '161'
+ self.node.driver_info['irmc_snmp_port'] = '161p'
self.assertRaises(exception.InvalidParameterValue,
irmc_common.parse_driver_info, self.node)
@@ -144,18 +224,164 @@ class IRMCValidateParametersTestCase(BaseIRMCTest):
self.assertRaises(exception.InvalidParameterValue,
irmc_common.parse_driver_info, self.node)
+ def test_parse_driver_info_missing_snmp_user(self):
+ self.node.driver_info['irmc_snmp_version'] = 'v3'
+ self.node.driver_info['irmc_snmp_auth_password'] = 'valid_key'
+ self.node.driver_info['irmc_snmp_priv_password'] = 'valid_key'
+ self.assertRaises(exception.MissingParameterValue,
+ irmc_common.parse_driver_info, self.node)
+
+ @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__'])
+ def test_parse_driver_info_missing_snmp_auth_password(self,
+ mock_scci_module):
+ self.node.driver_info['irmc_snmp_version'] = 'v3'
+ self.node.driver_info['irmc_snmp_user'] = 'admin0'
+ self.node.driver_info['irmc_snmp_priv_password'] = 'valid_key'
+ scci_version_list = ['0.10.1', '0.11.3', '0.12.2']
+ for ver in scci_version_list:
+ with self.subTest(ver=ver):
+ mock_scci_module.__version__ = ver
+ self.assertRaises(exception.MissingParameterValue,
+ irmc_common.parse_driver_info, self.node)
+
+ @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__'])
+ def test_parse_driver_info_missing_snmp_priv_password(self,
+ mock_scci_module):
+ self.node.driver_info['irmc_snmp_version'] = 'v3'
+ self.node.driver_info['irmc_snmp_user'] = 'admin0'
+ self.node.driver_info['irmc_snmp_auth_password'] = 'valid_key'
+ scci_version_list = ['0.10.1', '0.11.3', '0.12.2']
+ for ver in scci_version_list:
+ with self.subTest(ver=ver):
+ mock_scci_module.__version__ = ver
+ self.assertRaises(exception.MissingParameterValue,
+ irmc_common.parse_driver_info, self.node)
+
+ @mock.patch.object(irmc_common, 'LOG', autospec=True)
+ @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__'])
+ def test_parse_driver_info_ignoring_snmp_security(self, mock_scci_module,
+ mock_LOG):
+ self.node.driver_info['irmc_snmp_version'] = 'v3'
+ self.node.driver_info['irmc_snmp_user'] = 'admin0'
+ self.node.driver_info['irmc_snmp_security'] = 'security'
+ self.node.driver_info['irmc_snmp_auth_password'] = 'valid_key'
+ self.node.driver_info['irmc_snmp_priv_password'] = 'valid_key'
+ mock_scci_module.__version__ = '0.11.3'
+ info = irmc_common.parse_driver_info(self.node)
+ self.assertEqual('admin0', info['irmc_snmp_user'])
+ mock_LOG.warning.assert_called_once()
+ mock_LOG.warning.reset_mock
+
+ @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__'])
+ def test_parse_driver_info_using_snmp_security_(self, mock_scci_module):
+ self.node.driver_info['irmc_snmp_version'] = 'v3'
+ self.node.driver_info['irmc_snmp_security'] = 'admin0'
+ self.node.driver_info['irmc_snmp_auth_password'] = 'valid_key'
+ self.node.driver_info['irmc_snmp_priv_password'] = 'valid_key'
+ mock_scci_module.__version__ = '0.11.3'
+ info = irmc_common.parse_driver_info(self.node)
+ self.assertEqual('admin0', info['irmc_snmp_user'])
+
def test_parse_driver_info_invalid_snmp_security(self):
self.node.driver_info['irmc_snmp_version'] = 'v3'
self.node.driver_info['irmc_snmp_security'] = 100
+ self.node.driver_info['irmc_snmp_auth_password'] = 'valid_key'
+ self.node.driver_info['irmc_snmp_priv_password'] = 'valid_key'
self.assertRaises(exception.InvalidParameterValue,
irmc_common.parse_driver_info, self.node)
- def test_parse_driver_info_empty_snmp_security(self):
+ def test_parse_driver_info_invalid_snmp_user(self):
self.node.driver_info['irmc_snmp_version'] = 'v3'
- self.node.driver_info['irmc_snmp_security'] = ''
+ self.node.driver_info['irmc_snmp_user'] = 100
+ self.node.driver_info['irmc_snmp_auth_password'] = 'valid_key'
+ self.node.driver_info['irmc_snmp_priv_password'] = 'valid_key'
self.assertRaises(exception.InvalidParameterValue,
irmc_common.parse_driver_info, self.node)
+ @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__'])
+ def test_parse_driver_info_invalid_snmp_auth_password(self,
+ mock_scci_module):
+ self.node.driver_info['irmc_snmp_version'] = 'v3'
+ self.node.driver_info['irmc_snmp_user'] = 'admin0'
+ self.node.driver_info['irmc_snmp_auth_password'] = 100
+ self.node.driver_info['irmc_snmp_priv_password'] = 'valid_key'
+ scci_version_list = ['0.10.1', '0.11.3', '0.12.2']
+ for ver in scci_version_list:
+ with self.subTest(ver=ver):
+ mock_scci_module.__version__ = ver
+ self.assertRaises(exception.InvalidParameterValue,
+ irmc_common.parse_driver_info, self.node)
+
+ @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__'])
+ def test_parse_driver_info_short_snmp_auth_password(self,
+ mock_scci_module):
+ self.node.driver_info['irmc_snmp_version'] = 'v3'
+ self.node.driver_info['irmc_snmp_user'] = 'admin0'
+ self.node.driver_info['irmc_snmp_auth_password'] = 'short'
+ self.node.driver_info['irmc_snmp_priv_password'] = 'valid_key'
+ scci_version_list = ['0.10.1', '0.11.3', '0.12.2']
+ for ver in scci_version_list:
+ with self.subTest(ver=ver):
+ mock_scci_module.__version__ = ver
+ self.assertRaises(exception.InvalidParameterValue,
+ irmc_common.parse_driver_info, self.node)
+
+ @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__'])
+ def test_parse_driver_info_invalid_snmp_priv_password(self,
+ mock_scci_module):
+ self.node.driver_info['irmc_snmp_version'] = 'v3'
+ self.node.driver_info['irmc_snmp_user'] = 'admin0'
+ self.node.driver_info['irmc_snmp_auth_password'] = 'valid_key'
+ self.node.driver_info['irmc_snmp_priv_password'] = 100
+ scci_version_list = ['0.10.1', '0.11.3', '0.12.2']
+ for ver in scci_version_list:
+ with self.subTest(ver=ver):
+ mock_scci_module.__version__ = ver
+ self.assertRaises(exception.InvalidParameterValue,
+ irmc_common.parse_driver_info, self.node)
+
+ @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__'])
+ def test_parse_driver_info_short_snmp_priv_password(self,
+ mock_scci_module):
+ self.node.driver_info['irmc_snmp_version'] = 'v3'
+ self.node.driver_info['irmc_snmp_user'] = 'admin0'
+ self.node.driver_info['irmc_snmp_auth_password'] = 'valid_key'
+ self.node.driver_info['irmc_snmp_priv_password'] = 'short'
+ scci_version_list = ['0.10.1', '0.11.3', '0.12.2']
+ for ver in scci_version_list:
+ with self.subTest(ver=ver):
+ mock_scci_module.__version__ = ver
+ self.assertRaises(exception.InvalidParameterValue,
+ irmc_common.parse_driver_info, self.node)
+
+ @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__'])
+ def test_parse_driver_info_invalid_snmp_auth_proto(self, mock_scci_module):
+ self.node.driver_info['irmc_snmp_version'] = 'v3'
+ self.node.driver_info['irmc_snmp_user'] = 'admin0'
+ self.node.driver_info['irmc_snmp_auth_password'] = 'valid_key'
+ self.node.driver_info['irmc_snmp_priv_password'] = 'valid_key'
+ self.node.driver_info['irmc_snmp_auth_proto'] = 'invalid'
+ scci_version_list = ['0.10.1', '0.11.3', '0.12.2']
+ for ver in scci_version_list:
+ with self.subTest(ver=ver):
+ mock_scci_module.__version__ = ver
+ self.assertRaises(exception.InvalidParameterValue,
+ irmc_common.parse_driver_info, self.node)
+
+ @mock.patch.object(irmc_common, 'scci_mod', spec_set=['__version__'])
+ def test_parse_driver_info_invalid_snmp_priv_proto(self, mock_scci_module):
+ self.node.driver_info['irmc_snmp_version'] = 'v3'
+ self.node.driver_info['irmc_snmp_user'] = 'admin0'
+ self.node.driver_info['irmc_snmp_auth_password'] = 'valid_key'
+ self.node.driver_info['irmc_snmp_priv_password'] = 'valid_key'
+ self.node.driver_info['irmc_snmp_priv_proto'] = 'invalid'
+ scci_version_list = ['0.10.1', '0.11.3', '0.12.2']
+ for ver in scci_version_list:
+ with self.subTest(ver=ver):
+ mock_scci_module.__version__ = ver
+ self.assertRaises(exception.InvalidParameterValue,
+ irmc_common.parse_driver_info, self.node)
+
@mock.patch.object(os.path, 'isabs', return_value=True, autospec=True)
@mock.patch.object(os.path, 'isdir', return_value=True, autospec=True)
def test_parse_driver_info_dir_path_verify_ca(self, mock_isdir,
diff --git a/releasenotes/notes/irmc-add-snmpv3-security-fca05bfc30f50d1a.yaml b/releasenotes/notes/irmc-add-snmpv3-security-fca05bfc30f50d1a.yaml
new file mode 100644
index 000000000..296905819
--- /dev/null
+++ b/releasenotes/notes/irmc-add-snmpv3-security-fca05bfc30f50d1a.yaml
@@ -0,0 +1,28 @@
+---
+fixes:
+ - |
+ Fixes SNMPv3 message authentication and encryption functionality of iRMC
+ driver. The SNMPv3 authentication between iRMC driver and iRMC was only
+ by the security name with no passwords and encryption.
+ To increase security, the following parameters are now added to the node's
+ ``driver_info``, and can be used for authentication:
+
+ * ``irmc_snmp_user``
+ * ``irmc_snmp_auth_password``
+ * ``irmc_snmp_priv_password``
+ * ``irmc_snmp_auth_proto`` (Optional, defaults to ``sha``)
+ * ``irmc_snmp_priv_proto`` (Optional, defaults to ``aes``)
+
+ ``irmc_snmp_user`` replaces ``irmc_snmp_security``. ``irmc_snmp_security``
+ will be ignored if ``irmc_snmp_user`` is set.
+ ``irmc_snmp_auth_proto`` and ``irmc_snmp_priv_proto`` can also be set
+ through the following options in the ``[irmc]`` section of
+ ``/etc/ironic/ironic.conf``:
+
+ * ``snmp_auth_proto``
+ * ``snmp_priv_proto``
+
+other:
+ - |
+ Updates the minimum version of ``python-scciclient`` library to
+ ``0.11.3``.