SNMPv3 security features added to the snmp driver

SNMPv3 message authentication and encryption features
added to Ironic ``snmp`` driver.

Added support for the ``context_engine_id`` and
``context_name`` parameters of the SNMPv3 message
at Ironic's ``snmp`` driver.

The ``snmp_security`` parameter of Ironic ``snmp``
driver is obsoleted by the ``snmp_user`` parameter.
Though ``snmp_security`` parameter remains supported.

For the sake of patch reviewability, it does not touch
similar SNMP features in the `iLO` and `irmc` drivers.
A followup patch might converge these similar SNMP uses
onto the core ``snmp`` driver's functionality this patch
introduces.

Change-Id: Ic8a4fc37a42377c4ec50ffae421b3f47922ef982
Story: 1710850
Task: 10611
This commit is contained in:
Ilya Etingof 2018-02-26 12:32:37 +01:00
parent 96aeaa8d9b
commit 635f4a9e17
6 changed files with 469 additions and 39 deletions

View File

@ -124,10 +124,28 @@ The following property values have to be added to the node's
- ``snmp_version``: (optional) SNMP protocol version - ``snmp_version``: (optional) SNMP protocol version
(permitted values ``1``, ``2c`` or ``3``). If not specified, SNMPv1 (permitted values ``1``, ``2c`` or ``3``). If not specified, SNMPv1
is chosen. is chosen.
- ``snmp_community``: (Required for SNMPv1 and SNMPv2c) SNMP community - ``snmp_community``: (Required for SNMPv1/SNMPv2c) SNMP community
parameter for reads and writes to the PDU. name parameter for reads and writes to the PDU.
- ``snmp_security``: (Required for SNMPv3) SNMPv3 User-based Security Model - ``snmp_user``: (Required for SNMPv3) SNMPv3 User-based Security Model
(USM) user name. (USM) user name. Synonym for now obsolete ``snmp_security`` parameter.
- ``snmp_auth_protocol``: SNMPv3 message authentication protocol ID.
Valid values include: ``none``, ``md5``, ``sha`` for all pysnmp versions
and additionally ``sha224``, ``sha256``, ``sha384``, ``sha512`` for
pysnmp versions 4.4.1 and later. Default is ``none`` unless ``snmp_auth_key``
is provided. In the latter case ``md5`` is the default.
- ``snmp_auth_key``: SNMPv3 message authentication key. Must be 8+
characters long. Required when message authentication is used.
- ``snmp_priv_protocol``: SNMPv3 message privacy (encryption) protocol ID.
Valid values include: ``none``, ``des``, ``3des``, ``aes``, ``aes192``,
``aes256`` for all pysnmp version and additionally ``aes192blmt``,
``aes256blmt`` for pysnmp versions 4.4.3+. Note that message privacy
requires using message authentication. Default is ``none`` unless
``snmp_priv_key`` is provided. In the latter case ``des`` is the default.
- ``snmp_priv_key``: SNMPv3 message privacy (encryption) key. Must be 8+
characters long. Required when message encryption is used.
- ``snmp_context_engine_id``: SNMPv3 context engine ID. Default is
the value of authoritative engine ID.
- ``snmp_context_name``: SNMPv3 context name. Default is an empty string.
The following command can be used to enroll a node with the ``snmp`` hardware The following command can be used to enroll a node with the ``snmp`` hardware
type: type:
@ -140,12 +158,3 @@ type:
--driver-info snmp_outlet=<outlet_index> \ --driver-info snmp_outlet=<outlet_index> \
--driver-info snmp_community=<community_string> \ --driver-info snmp_community=<community_string> \
--properties capabilities=boot_option:netboot --properties capabilities=boot_option:netboot
PDU Configuration
=================
This version of the SNMP power interface does not support SNMPv3 authentication
or encryption features. When using SNMPv3, the SNMPv3 agent at the PDU must
be configured in ``noAuthNoPriv`` mode. Also, the ``snmp_security`` parameter
is used to configure SNMP USM user name to the SNMP manager at the power
interface. The same USM user name must be configured to the target SNMP agent.

View File

@ -44,11 +44,63 @@ if pysnmp:
from pysnmp.entity.rfc3413.oneliner import cmdgen from pysnmp.entity.rfc3413.oneliner import cmdgen
from pysnmp import error as snmp_error from pysnmp import error as snmp_error
from pysnmp.proto import rfc1902 from pysnmp.proto import rfc1902
snmp_auth_protocols = {
'md5': cmdgen.usmHMACMD5AuthProtocol,
'sha': cmdgen.usmHMACSHAAuthProtocol,
'none': cmdgen.usmNoAuthProtocol,
}
# available since pysnmp 4.4.1
try:
snmp_auth_protocols.update(
{
'sha224': cmdgen.usmHMAC128SHA224AuthProtocol,
'sha256': cmdgen.usmHMAC192SHA256AuthProtocol,
'sha384': cmdgen.usmHMAC256SHA384AuthProtocol,
'sha512': cmdgen.usmHMAC384SHA512AuthProtocol,
}
)
except AttributeError:
pass
snmp_priv_protocols = {
'des': cmdgen.usmDESPrivProtocol,
'3des': cmdgen.usm3DESEDEPrivProtocol,
'aes': cmdgen.usmAesCfb128Protocol,
'aes192': cmdgen.usmAesCfb192Protocol,
'aes256': cmdgen.usmAesCfb256Protocol,
'none': cmdgen.usmNoPrivProtocol,
}
# available since pysnmp 4.4.3
try:
snmp_priv_protocols.update(
{
'aes192blmt': cmdgen.usmAesBlumenthalCfb192Protocol,
'aes256blmt': cmdgen.usmAesBlumenthalCfb256Protocol,
}
)
except AttributeError:
pass
else: else:
cmdgen = None cmdgen = None
snmp_error = None snmp_error = None
rfc1902 = None rfc1902 = None
snmp_auth_protocols = {
'none': None
}
snmp_priv_protocols = {
'none': None
}
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -65,20 +117,60 @@ REQUIRED_PROPERTIES = {
OPTIONAL_PROPERTIES = { OPTIONAL_PROPERTIES = {
'snmp_version': 'snmp_version':
_("SNMP protocol version: %(v1)s, %(v2c)s or %(v3)s " _("SNMP protocol version: %(v1)s, %(v2c)s or %(v3)s "
"(optional, default %(v1)s)") "(optional, default %(v1)s).")
% {"v1": SNMP_V1, "v2c": SNMP_V2C, "v3": SNMP_V3}, % {"v1": SNMP_V1, "v2c": SNMP_V2C, "v3": SNMP_V3},
'snmp_port': 'snmp_port':
_("SNMP port, default %(port)d") % {"port": SNMP_PORT}, _("SNMP port, default %(port)d.") % {"port": SNMP_PORT},
'snmp_community': 'snmp_community':
_("SNMP community. Required for versions %(v1)s and %(v2c)s") _("SNMP community. Required for versions %(v1)s and %(v2c)s.")
% {"v1": SNMP_V1, "v2c": SNMP_V2C}, % {"v1": SNMP_V1, "v2c": SNMP_V2C},
'snmp_user':
_("SNMPv3 User-based Security Model (USM) username. "
"Required for version %(v3)s.")
% {"v3": SNMP_V3},
'snmp_auth_protocol':
_("SNMPv3 message authentication protocol ID. "
"Known values are: %(auth)s. "
"Default is 'none' unless 'snmp_auth_key' is provided. "
"In the latter case 'md5' is the default.")
% {'auth': sorted(snmp_auth_protocols)},
'snmp_auth_key':
_("SNMPv3 message authentication key. "
"Must be 8+ characters long. "
"Required when message authentication is used. "
"This key is used by the 'snmp_auth_protocol' algorithm."),
'snmp_priv_protocol':
_("SNMPv3 message privacy (encryption) protocol ID. "
"Known values are: %(priv)s. "
"Using message privacy requires using message authentication. "
"Default is 'none' unless 'snmp_priv_key' is provided. "
"In the latter case 'des' is the default.")
% {'priv': sorted(snmp_priv_protocols)},
'snmp_priv_key':
_("SNMPv3 message authentication key. "
"Must be 8+ characters long. "
"Required when message authentication is used. "
"This key is used by the 'snmp_priv_protocol' algorithm."),
'snmp_context_engine_id':
_("SNMPv3 context engine ID. "
"Default is the value of authoritative engine ID."),
'snmp_context_name':
_("SNMPv3 context name. "
"Default is an empty string ('')."),
}
DEPRECATED_PROPERTIES = {
# synonym for `snmp_user`
'snmp_security': 'snmp_security':
_("SNMPv3 User-based Security Model (USM) username. " _("SNMPv3 User-based Security Model (USM) username. "
"Required for version %(v3)s") "Required for version %(v3)s. "
"This property is deprecated, please use `snmp_user` instead.")
% {"v3": SNMP_V3}, % {"v3": SNMP_V3},
} }
COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy() COMMON_PROPERTIES = REQUIRED_PROPERTIES.copy()
COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES) COMMON_PROPERTIES.update(OPTIONAL_PROPERTIES)
COMMON_PROPERTIES.update(DEPRECATED_PROPERTIES)
class SNMPClient(object): class SNMPClient(object):
@ -88,27 +180,50 @@ class SNMPClient(object):
interaction with PySNMP to simplify dynamic importing and unit testing. interaction with PySNMP to simplify dynamic importing and unit testing.
""" """
def __init__(self, address, port, version, community=None, security=None): def __init__(self, address, port, version, community=None,
user=None, auth_proto=None,
auth_key=None, priv_proto=None,
priv_key=None, context_engine_id=None, context_name=None):
if not cmdgen:
raise exception.DriverLoadError(
driver=self.__class__.__name__,
reason=_("Unable to import python-pysnmp library")
)
self.address = address self.address = address
self.port = port self.port = port
self.version = version self.version = version
if self.version == SNMP_V3: if self.version == SNMP_V3:
self.security = security self.user = user
self.auth_proto = auth_proto
self.auth_key = auth_key
self.priv_proto = priv_proto
self.priv_key = priv_key
self.context_engine_id = context_engine_id
self.context_name = context_name or ''
else: else:
self.community = community self.community = community
self.cmd_gen = cmdgen.CommandGenerator() self.cmd_gen = cmdgen.CommandGenerator()
def _get_auth(self): def _get_auth(self):
"""Return the authorization data for an SNMP request. """Return the authorization data for an SNMP request.
:returns: A :returns: Either
:class:`pysnmp.entity.rfc3413.oneliner.cmdgen.CommunityData` :class:`pysnmp.entity.rfc3413.oneliner.cmdgen.CommunityData`
object. or :class:`pysnmp.entity.rfc3413.oneliner.cmdgen.UsmUserData`
object depending on SNMP version being used.
""" """
if self.version == SNMP_V3: if self.version == SNMP_V3:
# Handling auth/encryption credentials is not (yet) supported. return cmdgen.UsmUserData(
# This version supports a security name analogous to community. self.user,
return cmdgen.UsmUserData(self.security) authKey=self.auth_key,
authProtocol=self.auth_proto,
privKey=self.priv_key,
privProtocol=self.priv_proto,
contextEngineId=self.context_engine_id,
contextName=self.context_name
)
else: else:
mp_model = 1 if self.version == SNMP_V2C else 0 mp_model = 1 if self.version == SNMP_V2C else 0
return cmdgen.CommunityData(self.community, mpModel=mp_model) return cmdgen.CommunityData(self.community, mpModel=mp_model)
@ -223,7 +338,13 @@ def _get_client(snmp_info):
snmp_info["port"], snmp_info["port"],
snmp_info["version"], snmp_info["version"],
snmp_info.get("community"), snmp_info.get("community"),
snmp_info.get("security")) snmp_info.get("user"),
snmp_info.get("auth_proto"),
snmp_info.get("auth_key"),
snmp_info.get("priv_proto"),
snmp_info.get("priv_key"),
snmp_info.get("context_engine_id"),
snmp_info.get("context_name"))
@six.add_metaclass(abc.ABCMeta) @six.add_metaclass(abc.ABCMeta)
@ -580,6 +701,124 @@ DRIVER_CLASSES = {
} }
def _parse_driver_info_snmpv3_user(node, info):
snmp_info = {}
if 'snmp_user' not in info and 'snmp_security' not in info:
raise exception.MissingParameterValue(_(
"SNMP driver requires `driver_info/snmp_user` to be set in "
"node %(node)s configuration for SNMP version %(ver)s.") %
{'node': node.uuid, 'ver': SNMP_V3})
snmp_info['user'] = info.get('snmp_user', info.get('snmp_security'))
if 'snmp_security' in info:
LOG.warning("The `driver_info/snmp_security` parameter is deprecated "
"in favor of `driver_info/snmp_user` parameter. Please "
"remove the `driver_info/snmp_security` parameter from "
"node %(node)s configuration.", {'node': node.uuid})
if 'snmp_user' in info:
LOG.warning("The `driver_info/snmp_security` parameter is ignored "
"in favor of `driver_info/snmp_user` parameter in "
"node %(node)s configuration.", {'node': node.uuid})
return snmp_info
def _parse_driver_info_snmpv3_crypto(node, info):
snmp_info = {}
if 'snmp_auth_protocol' in info:
auth_p = info['snmp_auth_protocol']
try:
snmp_info['auth_protocol'] = snmp_auth_protocols[auth_p]
except KeyError:
raise exception.InvalidParameterValue(_(
"SNMPPowerDriver: unknown SNMPv3 authentication protocol "
"`driver_info/snmp_auth_protocol` %(proto)s in node %(node)s "
"configuration, known protocols are: %(protos)s") %
{'node': node.uuid, 'proto': auth_p,
'protos': ', '.join(snmp_auth_protocols)}
)
if 'snmp_priv_protocol' in info:
priv_p = info['snmp_priv_protocol']
try:
snmp_info['priv_protocol'] = snmp_priv_protocols[priv_p]
except KeyError:
raise exception.InvalidParameterValue(_(
"SNMPPowerDriver: unknown SNMPv3 privacy protocol "
"`driver_info/snmp_priv_protocol` %(proto)s in node "
"%(node)s configuration, known protocols are: %(protos)s") %
{'node': node.uuid, 'proto': priv_p,
'protos': ', '.join(snmp_priv_protocols)}
)
if 'snmp_auth_key' in info:
auth_k = info['snmp_auth_key']
if len(auth_k) < 8:
raise exception.InvalidParameterValue(_(
"SNMPPowerDriver: short SNMPv3 authentication key "
"`driver_info/snmp_auth_key` in node %(node)s configuration "
"(8+ chars required)") % {'node': node.uuid})
snmp_info['auth_key'] = auth_k
if 'auth_protocol' not in snmp_info:
snmp_info['auth_protocol'] = snmp_auth_protocols['md5']
if 'snmp_priv_key' in info:
priv_k = info['snmp_priv_key']
if len(priv_k) < 8:
raise exception.InvalidParameterValue(_(
"SNMPPowerDriver: short SNMPv3 privacy key "
"`driver_info/snmp_priv_key` node %(node)s configuration "
"(8+ chars required)") % {'node': node.uuid})
snmp_info['priv_key'] = priv_k
if 'priv_protocol' not in snmp_info:
snmp_info['priv_protocol'] = snmp_priv_protocols['des']
if ('priv_protocol' in snmp_info and
'auth_protocol' not in snmp_info):
raise exception.MissingParameterValue(_(
"SNMPPowerDriver: SNMPv3 privacy requires authentication. "
"Please add `driver_info/auth_protocol` property to node "
"%(node)s configuration.") % {'node': node.uuid})
if ('auth_protocol' in snmp_info and
'auth_key' not in snmp_info):
raise exception.MissingParameterValue(_(
"SNMPPowerDriver: missing SNMPv3 authentication key while "
"`driver_info/snmp_auth_protocol` is present. Please "
"add `driver_info/snmp_auth_key` to node %(node)s "
"configuration.") % {'node': node.uuid})
if ('priv_protocol' in snmp_info and
'priv_key' not in snmp_info):
raise exception.MissingParameterValue(_(
"SNMPPowerDriver: missing SNMPv3 privacy key while "
"`driver_info/snmp_priv_protocol` is present. Please"
"add `driver_info/snmp_priv_key` to node %(node)s "
"configuration.") % {'node': node.uuid})
return snmp_info
def _parse_driver_info_snmpv3_context(node, info):
snmp_info = {}
if 'snmp_context_engine_id' in info:
snmp_info['context_engine_id'] = info['snmp_context_engine_id']
if 'snmp_context_name' in info:
snmp_info['context_name'] = info['snmp_context_name']
return snmp_info
def _parse_driver_info(node): def _parse_driver_info(node):
"""Parse a node's driver_info values. """Parse a node's driver_info values.
@ -630,11 +869,9 @@ def _parse_driver_info(node):
"%s.") % snmp_info['version']) "%s.") % snmp_info['version'])
snmp_info['community'] = info.get('snmp_community') snmp_info['community'] = info.get('snmp_community')
elif snmp_info['version'] == SNMP_V3: elif snmp_info['version'] == SNMP_V3:
if 'snmp_security' not in info: snmp_info.update(_parse_driver_info_snmpv3_user(node, info))
raise exception.MissingParameterValue(_( snmp_info.update(_parse_driver_info_snmpv3_crypto(node, info))
"SNMP driver requires snmp_security to be set for version %s.") snmp_info.update(_parse_driver_info_snmpv3_context(node, info))
% (SNMP_V3))
snmp_info['security'] = info.get('snmp_security')
# Target PDU IP address and power outlet identification # Target PDU IP address and power outlet identification
snmp_info['address'] = info['snmp_address'] snmp_info['address'] = info['snmp_address']

View File

@ -5781,6 +5781,10 @@ class ManagerTestProperties(mgr_utils.ServiceSetUpMixin, db_base.DbTestCase):
'force_persistent_boot_device', 'force_persistent_boot_device',
'snmp_driver', 'snmp_address', 'snmp_port', 'snmp_version', 'snmp_driver', 'snmp_address', 'snmp_port', 'snmp_version',
'snmp_community', 'snmp_security', 'snmp_outlet', 'snmp_community', 'snmp_security', 'snmp_outlet',
'snmp_user',
'snmp_context_engine_id', 'snmp_context_name',
'snmp_auth_key', 'snmp_auth_protocol',
'snmp_priv_key', 'snmp_priv_protocol',
'deploy_forces_oob_reboot'] 'deploy_forces_oob_reboot']
self._check_driver_properties("snmp", expected) self._check_driver_properties("snmp", expected)

View File

@ -137,7 +137,14 @@ def get_test_snmp_info(**kw):
if result["snmp_version"] in ("1", "2c"): if result["snmp_version"] in ("1", "2c"):
result["snmp_community"] = kw.get("snmp_community", "public") result["snmp_community"] = kw.get("snmp_community", "public")
elif result["snmp_version"] == "3": elif result["snmp_version"] == "3":
result["snmp_security"] = kw.get("snmp_security", "public") result["snmp_user"] = kw.get(
"snmp_user", kw.get("snmp_security", "snmpuser")
)
for option in ('snmp_auth_protocol', 'snmp_auth_key',
'snmp_priv_protocol', 'snmp_priv_key',
'snmp_context_engine_id', 'snmp_context_name'):
if option in kw:
result[option] = kw[option]
return result return result

View File

@ -52,7 +52,7 @@ class SNMPClientTestCase(base.TestCase):
self.assertEqual(self.port, client.port) self.assertEqual(self.port, client.port)
self.assertEqual(snmp.SNMP_V1, client.version) self.assertEqual(snmp.SNMP_V1, client.version)
self.assertIsNone(client.community) self.assertIsNone(client.community)
self.assertNotIn('security', client.__dict__) self.assertNotIn('user', client.__dict__)
self.assertEqual(mock_cmdgen.return_value, client.cmd_gen) self.assertEqual(mock_cmdgen.return_value, client.cmd_gen)
@mock.patch.object(cmdgen, 'CommunityData', autospec=True) @mock.patch.object(cmdgen, 'CommunityData', autospec=True)
@ -67,7 +67,15 @@ class SNMPClientTestCase(base.TestCase):
client = snmp.SNMPClient(self.address, self.port, snmp.SNMP_V3) client = snmp.SNMPClient(self.address, self.port, snmp.SNMP_V3)
client._get_auth() client._get_auth()
mock_cmdgen.assert_called_once_with() mock_cmdgen.assert_called_once_with()
mock_user.assert_called_once_with(client.security) mock_user.assert_called_once_with(
client.user,
authKey=client.auth_key,
authProtocol=client.auth_proto,
privKey=client.priv_key,
privProtocol=client.priv_proto,
contextEngineId=client.context_engine_id,
contextName=client.context_name
)
@mock.patch.object(cmdgen, 'UdpTransportTarget', autospec=True) @mock.patch.object(cmdgen, 'UdpTransportTarget', autospec=True)
def test__get_transport(self, mock_transport, mock_cmdgen): def test__get_transport(self, mock_transport, mock_cmdgen):
@ -237,7 +245,7 @@ class SNMPValidateParametersTestCase(db_base.DbTestCase):
self.assertEqual(INFO_DICT['snmp_outlet'], str(info['outlet'])) self.assertEqual(INFO_DICT['snmp_outlet'], str(info['outlet']))
self.assertEqual(INFO_DICT['snmp_version'], info['version']) self.assertEqual(INFO_DICT['snmp_version'], info['version'])
self.assertEqual(INFO_DICT['snmp_community'], info['community']) self.assertEqual(INFO_DICT['snmp_community'], info['community'])
self.assertNotIn('security', info) self.assertNotIn('user', info)
def test__parse_driver_info_apc(self): def test__parse_driver_info_apc(self):
# Make sure the APC driver type is parsed. # Make sure the APC driver type is parsed.
@ -314,13 +322,156 @@ class SNMPValidateParametersTestCase(db_base.DbTestCase):
self.assertEqual('private', info['community']) self.assertEqual('private', info['community'])
def test__parse_driver_info_snmp_v3(self): def test__parse_driver_info_snmp_v3(self):
# Make sure SNMPv3 is parsed with user string.
info = db_utils.get_test_snmp_info(snmp_version='3',
snmp_user='pass')
node = self._get_test_node(info)
info = snmp._parse_driver_info(node)
self.assertEqual('3', info['version'])
self.assertEqual('pass', info['user'])
def test__parse_driver_info_snmp_v3_auth_default_proto(self):
info = db_utils.get_test_snmp_info(snmp_version='3',
snmp_user='pass',
snmp_auth_key='12345678')
node = self._get_test_node(info)
info = snmp._parse_driver_info(node)
self.assertEqual('12345678', info['auth_key'])
self.assertEqual(snmp.snmp_auth_protocols['md5'],
info['auth_protocol'])
def test__parse_driver_info_snmp_v3_auth_key_proto(self):
info = db_utils.get_test_snmp_info(snmp_version='3',
snmp_user='pass',
snmp_auth_key='12345678',
snmp_auth_protocol='sha')
node = self._get_test_node(info)
info = snmp._parse_driver_info(node)
self.assertEqual('12345678', info['auth_key'])
self.assertEqual(snmp.snmp_auth_protocols['sha'],
info['auth_protocol'])
def test__parse_driver_info_snmp_v3_auth_nokey(self):
info = db_utils.get_test_snmp_info(snmp_version='3',
snmp_user='pass',
snmp_auth_protocol='sha')
node = self._get_test_node(info)
self.assertRaisesRegex(
exception.InvalidParameterValue,
'missing.*authentication key',
snmp._parse_driver_info,
node
)
def test__parse_driver_info_snmp_v3_auth_badproto(self):
info = db_utils.get_test_snmp_info(snmp_version='3',
snmp_user='pass',
snmp_auth_key='12345678',
snmp_auth_protocol='whatever')
node = self._get_test_node(info)
self.assertRaisesRegex(
exception.InvalidParameterValue,
'.*?unknown SNMPv3 authentication protocol.*',
snmp._parse_driver_info,
node
)
def test__parse_driver_info_snmp_v3_auth_short_key(self):
info = db_utils.get_test_snmp_info(snmp_version='3',
snmp_user='pass',
snmp_auth_key='1234567')
node = self._get_test_node(info)
self.assertRaisesRegex(
exception.InvalidParameterValue,
'.*?short SNMPv3 authentication key.*',
snmp._parse_driver_info,
node
)
def test__parse_driver_info_snmp_v3_priv_default_proto(self):
info = db_utils.get_test_snmp_info(snmp_version='3',
snmp_user='pass',
snmp_auth_key='12345678',
snmp_priv_key='87654321')
node = self._get_test_node(info)
info = snmp._parse_driver_info(node)
self.assertEqual('87654321', info['priv_key'])
self.assertEqual(snmp.snmp_priv_protocols['des'],
info['priv_protocol'])
def test__parse_driver_info_snmp_v3_priv_key_proto(self):
info = db_utils.get_test_snmp_info(snmp_version='3',
snmp_user='pass',
snmp_auth_key='12345678',
snmp_priv_protocol='3des',
snmp_priv_key='87654321')
node = self._get_test_node(info)
info = snmp._parse_driver_info(node)
self.assertEqual('87654321', info['priv_key'])
self.assertEqual(snmp.snmp_priv_protocols['3des'],
info['priv_protocol'])
def test__parse_driver_info_snmp_v3_priv_nokey(self):
info = db_utils.get_test_snmp_info(snmp_version='3',
snmp_user='pass',
snmp_priv_protocol='3des')
node = self._get_test_node(info)
self.assertRaisesRegex(
exception.InvalidParameterValue,
'.*?SNMPv3 privacy requires authentication.*',
snmp._parse_driver_info,
node
)
def test__parse_driver_info_snmp_v3_priv_badproto(self):
info = db_utils.get_test_snmp_info(snmp_version='3',
snmp_user='pass',
snmp_priv_key='12345678',
snmp_priv_protocol='whatever')
node = self._get_test_node(info)
self.assertRaisesRegex(
exception.InvalidParameterValue,
'.*?unknown SNMPv3 privacy protocol.*',
snmp._parse_driver_info,
node
)
def test__parse_driver_info_snmp_v3_priv_short_key(self):
info = db_utils.get_test_snmp_info(snmp_version='3',
snmp_user='pass',
snmp_priv_key='1234567')
node = self._get_test_node(info)
self.assertRaisesRegex(
exception.InvalidParameterValue,
'.*?short SNMPv3 privacy key.*',
snmp._parse_driver_info,
node
)
def test__parse_driver_info_snmp_v3_compat(self):
# Make sure SNMPv3 is parsed with a security string. # Make sure SNMPv3 is parsed with a security string.
info = db_utils.get_test_snmp_info(snmp_version='3', info = db_utils.get_test_snmp_info(snmp_version='3',
snmp_security='pass') snmp_security='pass')
node = self._get_test_node(info) node = self._get_test_node(info)
info = snmp._parse_driver_info(node) info = snmp._parse_driver_info(node)
self.assertEqual('3', info['version']) self.assertEqual('3', info['version'])
self.assertEqual('pass', info['security']) self.assertEqual('pass', info['user'])
def test__parse_driver_info_snmp_v3_context_engine_id(self):
info = db_utils.get_test_snmp_info(snmp_version='3',
snmp_user='pass',
snmp_context_engine_id='whatever')
node = self._get_test_node(info)
info = snmp._parse_driver_info(node)
self.assertEqual('whatever', info['context_engine_id'])
def test__parse_driver_info_snmp_v3_context_name(self):
info = db_utils.get_test_snmp_info(snmp_version='3',
snmp_user='pass',
snmp_context_name='whatever')
node = self._get_test_node(info)
info = snmp._parse_driver_info(node)
self.assertEqual('whatever', info['context_name'])
def test__parse_driver_info_snmp_port_default(self): def test__parse_driver_info_snmp_port_default(self):
# Make sure default SNMP UDP port numbers are correct # Make sure default SNMP UDP port numbers are correct
@ -394,7 +545,7 @@ class SNMPValidateParametersTestCase(db_base.DbTestCase):
# Make sure exception is raised when version is invalid. # Make sure exception is raised when version is invalid.
info = db_utils.get_test_snmp_info(snmp_version='42', info = db_utils.get_test_snmp_info(snmp_version='42',
snmp_community='public', snmp_community='public',
snmp_security='pass') snmp_user='pass')
node = self._get_test_node(info) node = self._get_test_node(info)
self.assertRaises(exception.InvalidParameterValue, self.assertRaises(exception.InvalidParameterValue,
snmp._parse_driver_info, snmp._parse_driver_info,
@ -428,10 +579,10 @@ class SNMPValidateParametersTestCase(db_base.DbTestCase):
snmp._parse_driver_info, snmp._parse_driver_info,
node) node)
def test__parse_driver_info_missing_security(self): def test__parse_driver_info_missing_user(self):
# Make sure exception is raised when security is missing with SNMPv3. # Make sure exception is raised when user is missing with SNMPv3.
info = db_utils.get_test_snmp_info(snmp_version='3') info = db_utils.get_test_snmp_info(snmp_version='3')
del info['snmp_security'] del info['snmp_user']
node = self._get_test_node(info) node = self._get_test_node(info)
self.assertRaises(exception.MissingParameterValue, self.assertRaises(exception.MissingParameterValue,
snmp._parse_driver_info, snmp._parse_driver_info,

View File

@ -0,0 +1,22 @@
---
features:
- |
Adds SNMPv3 message authentication and encryption features to ironic
``snmp`` hardware type. To enable these features, the following
parameters should be used in the node's ``driver_info``:
* ``snmp_user``
* ``snmp_auth_protocol``
* ``snmp_auth_key``
* ``snmp_priv_protocol``
* ``snmp_priv_key``
Also adds support for the ``context_engine_id`` and ``context_name``
parameters of SNMPv3 message at ironic ``snmp`` hardware type. They
can be configured in the node's ``driver_info``.
deprecations:
- |
Deprecates the ``snmp_security`` field in ``driver_info`` for ironic
``snmp`` hardware type, it will be removed in Stein release. Please use
``snmp_user`` field instead.