diff --git a/doc/source/admin/drivers/irmc.rst b/doc/source/admin/drivers/irmc.rst index 47ba7e9e6c..15d245c105 100644 --- a/doc/source/admin/drivers/irmc.rst +++ b/doc/source/admin/drivers/irmc.rst @@ -140,6 +140,16 @@ Node configuration ``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. + * All of the nodes are configured by setting the following configuration options in the ``[irmc]`` section of ``/etc/ironic/ironic.conf``: @@ -175,6 +185,18 @@ Node configuration 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. + + .. warning:: + We deprecated the ``snmp_security`` option when use SNMPv3 inspection. + Support for this option will be removed in the future. Instead, set + ``driver_info/irmc_snmp_user`` parameter for each node if SNMPv3 + inspection is needed. * Each node can be further configured by setting the following ironic node object's properties which override the parameter values in @@ -188,6 +210,10 @@ Node configuration - ``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/driver-requirements.txt b/driver-requirements.txt index da312468e6..5333dbd4f5 100644 --- a/driver-requirements.txt +++ b/driver-requirements.txt @@ -6,7 +6,7 @@ # These are available on pypi proliantutils>=2.13.0 pysnmp>=4.3.0,<5.0.0 -python-scciclient>=0.8.0 +python-scciclient>=0.12.2 python-dracclient>=5.1.0,<9.0.0 python-xclarityclient>=0.1.6 diff --git a/ironic/common/utils.py b/ironic/common/utils.py index fcf8389659..e4d83c9b6f 100644 --- a/ironic/common/utils.py +++ b/ironic/common/utils.py @@ -669,3 +669,15 @@ def fast_track_enabled(node): except ValueError as exc: raise exception.InvalidParameterValue( _("Invalid value of fast_track: %s") % exc) + + +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 839c9c27ea..7c319e2d8d 100644 --- a/ironic/conf/irmc.py +++ b/ironic/conf/irmc.py @@ -73,10 +73,22 @@ 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'."), + deprecated_for_removal=True, + deprecated_reason=_("Use irmc_snmp_user")), 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'. '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'. '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 24adf0da8d..00b7c06255 100644 --- a/ironic/drivers/modules/irmc/common.py +++ b/ironic/drivers/modules/irmc/common.py @@ -22,6 +22,7 @@ from ironic.common import exception from ironic.common.i18n import _ from ironic.common import utils from ironic.conf import CONF +from ironic.drivers.modules import snmp scci = importutils.try_import('scciclient.irmc.scci') elcm = importutils.try_import('scciclient.irmc.elcm') @@ -44,18 +45,49 @@ OPTIONAL_PROPERTIES = { 'irmc_sensor_method': _("Sensor data retrieval method; either " "'ipmitool' or 'scci'. The default value is " "'ipmitool'. 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."), + 'irmc_snmp_priv_password': _("SNMPv3 message privacy key. Must be 8+ " + "characters long. Required when message " + "privacy is used."), +} + +SNMP_V3_OPTIONAL_PROPERTIES = { + 'irmc_snmp_auth_proto': _("SNMPv3 message authentication protocol ID. " + "Required for version 'v3'. " + "'sha' is supported."), + 'irmc_snmp_priv_proto': _("SNMPv3 message privacy (encryption) protocol " + "ID. Required for version 'v3'. " + "'aes' is supported."), +} + +SNMP_V3_DEPRECATED_PROPERTIES = { + 'irmc_snmp_security': _("SNMP security name required for version 'v3'. " + "Optional. Deprecated."), +} + + COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy() COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES) +COMMON_PROPERTIES.update(SNMP_PROPERTIES) +COMMON_PROPERTIES.update(SNMP_V3_REQUIRED_PROPERTIES) +COMMON_PROPERTIES.update(SNMP_V3_OPTIONAL_PROPERTIES) +COMMON_PROPERTIES.update(SNMP_V3_DEPRECATED_PROPERTIES) def parse_driver_info(node): @@ -105,37 +137,145 @@ 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.")) if error_msgs: msg = (_("The following errors were encountered while parsing " "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: + snmp_info.update(_parse_snmp_v3_info(node, info)) + + return snmp_info + + +def _parse_snmp_v3_info(node, info): + snmp_info = {} + missing_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'} + + security = info.get('irmc_snmp_security', CONF.irmc.get('snmp_security')) + for param in SNMP_V3_REQUIRED_PROPERTIES: + try: + snmp_info[param] = info[param] + except KeyError: + if param == 'irmc_snmp_user': + if not security: + missing_info.append(param) + else: + LOG.warning(_("'irmc_snmp_security' parameter is " + "deprecated in favor of 'irmc_snmp_user' " + "parameter. Please set 'irmc_snmp_user' " + "and remove 'irmc_snmp_security' for node " + "%s."), node.uuid) + # In iRMC, the username must start with a letter, so only + # a string can be a valid username and a string from a + # number is invalid. + if not isinstance(security, str): + raise exception.InvalidParameterValue(_( + "Value '%s' is not a string for " + "'irmc_snmp_security.") % + info['irmc_snmp_security']) + else: + snmp_info['irmc_snmp_user'] = security + security = None + else: + missing_info.append(param) + + if missing_info: + raise exception.MissingParameterValue(_( + "The following required SNMP parameters " + "are missing: %s") % missing_info) + + if security: + LOG.warning(_("'irmc_snmp_security' parameter is ignored in favor of " + "'irmc_snmp_user' parameter. Please remove " + "'irmc_snmp_security' from node %s " + "configuration."), node.uuid) + if not isinstance(snmp_info['irmc_snmp_user'], str): + raise exception.InvalidParameterValue(_( + "Value '%s' is not a string for 'irmc_snmp_user'.") % + info['irmc_snmp_user']) + + for param in snmp_keys: + if not isinstance(snmp_info[param], str): + raise exception.InvalidParameterValue(_( + "Value %(value)s is not a string for %(param)s.") % + {'param': param, 'value': snmp_info[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. diff --git a/ironic/drivers/modules/irmc/inspect.py b/ironic/drivers/modules/irmc/inspect.py index 7b785d2fbf..9b6bff5bc8 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 fbadffb5ee..28041d8350 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 2aebe4e56c..8d8e4aa3c1 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 1037d50897..1822b99652 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,8 +61,7 @@ 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', } diff --git a/ironic/tests/unit/drivers/modules/irmc/test_common.py b/ironic/tests/unit/drivers/modules/irmc/test_common.py index 3f0c3d94a9..7598fc16b2 100644 --- a/ironic/tests/unit/drivers/modules/irmc/test_common.py +++ b/ironic/tests/unit/drivers/modules/irmc/test_common.py @@ -22,8 +22,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 \ @@ -54,7 +56,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']) @@ -64,12 +68,38 @@ 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']) - def test_parse_driver_option_default(self): + def test_parse_driver_info_snmpv3(self): + 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' + 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(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", @@ -130,8 +160,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) @@ -141,15 +179,98 @@ 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) + + def test_parse_driver_info_missing_snmp_auth_password(self): + 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' + self.assertRaises(exception.MissingParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_missing_snmp_priv_password(self): + 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.assertRaises(exception.MissingParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_using_snmp_security(self): + 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' + 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) + + def test_parse_driver_info_invalid_snmp_auth_password(self): + 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' + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_short_snmp_auth_password(self): + 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' + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_invalid_snmp_priv_password(self): + 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 + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_short_snmp_priv_password(self): + 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' + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_invalid_snmp_auth_proto(self): + 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' + self.assertRaises(exception.InvalidParameterValue, + irmc_common.parse_driver_info, self.node) + + def test_parse_driver_info_invalid_snmp_priv_proto(self): + 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' self.assertRaises(exception.InvalidParameterValue, irmc_common.parse_driver_info, self.node) 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 0000000000..7a6bde3fdb --- /dev/null +++ b/releasenotes/notes/irmc-add-snmpv3-security-fca05bfc30f50d1a.yaml @@ -0,0 +1,30 @@ +--- +features: + - | + Adds SNMPv3 message authentication and encryption features to iRMC driver. + To enable these features, the following parameters should be used in the + node's ``driver_info``: + + * ``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_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`` + +deprecations: + - | + Deprecates the ``irmc_snmp_security`` field in ``driver_info`` for iRMC + driver, it will be removed in the future. Please use ``irmc_snmp_user`` + field instead. + +other: + - | + Updates the minimum version of ``python-scciclient`` library to + ``0.12.1``.