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 79f82c0262)
(cherry picked from commit c274231bf5)
This commit is contained in:
Shukun Song
2022-06-10 20:09:50 +09:00
parent a26509b7ae
commit ef0e33edf2
10 changed files with 551 additions and 60 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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