Add snmp driver auto discovery

When instantiated, this new universal snmp driver chooses one
concrete snmp driver selected the prefix value of the
`SNMPv2-MIB::sysObjectID` MIB object looked up at the device.

Co-Authored-By: Ilya Etingof <etingof@gmail.com>
Change-Id: I9bf01a0c567048d0169db281684d16a00e39f6b8
Story: #1635644
Task: #10529
This commit is contained in:
Philippe Godin 2016-10-20 11:52:46 -04:00 committed by Ilya Etingof
parent 04a5ef1d39
commit 80d6c14db7
4 changed files with 216 additions and 2 deletions

View File

@ -116,7 +116,9 @@ set to the hardware type ``snmp``.
The following property values have to be added to the node's
``driver_info`` field:
- ``snmp_driver``: PDU manufacturer driver
- ``snmp_driver``: PDU manufacturer driver name or ``auto`` to automatically
choose ironic snmp driver based on ``SNMPv2-MIB::sysObjectID`` value as
reported by PDU.
- ``snmp_address``: the IPv4 address of the PDU controlling this node.
- ``snmp_port``: (optional) A non-standard UDP port to use for SNMP operations.
If not specified, the default port (161) is used.

View File

@ -527,6 +527,7 @@ class SNMPDriverAten(SNMPDriverSimple):
1.3.6.1.4.1.21317.1.3.2.2.2.2 Outlet Power
Values: 1=Off, 2=On, 3=Pending, 4=Reset
"""
system_id = (21317,)
oid_device = (21317, 1, 3, 2, 2, 2, 2)
value_power_on = 2
value_power_off = 1
@ -548,6 +549,7 @@ class SNMPDriverAPCMasterSwitch(SNMPDriverSimple):
Values: 1=On, 2=Off, 3=PowerCycle, [...more options follow]
"""
system_id = (318, 1, 1, 4)
oid_device = (318, 1, 1, 4, 4, 2, 1, 3)
value_power_on = 1
value_power_off = 2
@ -561,6 +563,7 @@ class SNMPDriverAPCMasterSwitchPlus(SNMPDriverSimple):
Values: 1=On, 3=Off, [...more options follow]
"""
system_id = (318, 1, 1, 6)
oid_device = (318, 1, 1, 6, 5, 1, 1, 5)
value_power_on = 1
value_power_off = 3
@ -574,6 +577,7 @@ class SNMPDriverAPCRackPDU(SNMPDriverSimple):
Values: 1=On, 2=Off, 3=PowerCycle, [...more options follow]
"""
system_id = (318, 1, 1, 12)
oid_device = (318, 1, 1, 12, 3, 3, 1, 1, 4)
value_power_on = 1
value_power_off = 2
@ -591,6 +595,7 @@ class SNMPDriverCyberPower(SNMPDriverSimple):
# been implemented based upon its published MIB
# documentation.
system_id = (3808,)
oid_device = (3808, 1, 1, 3, 3, 3, 1, 1, 4)
value_power_on = 1
value_power_off = 2
@ -604,6 +609,7 @@ class SNMPDriverTeltronix(SNMPDriverSimple):
Values: 1=Off, 2=On
"""
system_id = (23620,)
oid_device = (23620, 1, 2, 2, 1, 4)
value_power_on = 2
value_power_off = 1
@ -628,6 +634,7 @@ class SNMPDriverEatonPower(SNMPDriverBase):
# been implemented based upon its published MIB
# documentation.
system_id = (534,)
oid_device = (534, 6, 6, 7, 6, 6, 1)
oid_status = (2,)
oid_poweron = (3,)
@ -688,6 +695,66 @@ class SNMPDriverEatonPower(SNMPDriverBase):
self.client.set(oid, value)
class SNMPDriverAuto(SNMPDriverBase):
SYS_OBJ_OID = (1, 3, 6, 1, 2, 1, 1, 2)
def __init__(self, *args, **kwargs):
super(SNMPDriverAuto, self).__init__(*args, **kwargs)
drivers_map = {}
for name, obj in DRIVER_CLASSES.items():
if not getattr(obj, 'system_id', False):
continue
system_id = self.oid_enterprise + getattr(obj, 'system_id')
if (system_id in drivers_map and
drivers_map[system_id] is not obj):
raise exception.InvalidParameterValue(_(
"SNMPDriverAuto: duplicate driver system ID prefix "
"%(system_id)s") % {'system_id': system_id})
drivers_map[system_id] = obj
LOG.debug("SNMP driver mapping %(system_id)s -> %(name)s",
{'system_id': system_id, 'name': obj.__name__})
system_id = self.client.get(self.SYS_OBJ_OID)
LOG.debug("SNMP device reports sysObjectID %(system_id)s",
{'system_id': system_id})
system_id_prefix = tuple(system_id)
# pick driver by the longest matching sysObjectID prefix
while len(system_id_prefix) > len(self.oid_enterprise):
try:
Driver = drivers_map[system_id_prefix]
LOG.debug("Chosen SNMP driver %(name)s based on sysObjectID "
"prefix %(system_id_prefix)s", {Driver.__name__,
system_id_prefix})
self.driver = Driver(*args, **kwargs)
return
except KeyError:
system_id_prefix = system_id_prefix[:-1]
raise exception.InvalidParameterValue(_(
"SNMPDriverAuto: no driver matching %(system_id)s") %
{'system_id': system_id})
def _snmp_power_state(self):
current_power_state = self.driver._snmp_power_state()
return current_power_state
def _snmp_power_on(self):
return self.driver._snmp_power_on()
def _snmp_power_off(self):
return self.driver._snmp_power_off()
# A dictionary of supported drivers keyed by snmp_driver attribute
DRIVER_CLASSES = {
'apc': SNMPDriverAPCMasterSwitch,
@ -697,7 +764,8 @@ DRIVER_CLASSES = {
'aten': SNMPDriverAten,
'cyberpower': SNMPDriverCyberPower,
'eatonpower': SNMPDriverEatonPower,
'teltronix': SNMPDriverTeltronix
'teltronix': SNMPDriverTeltronix,
'auto': SNMPDriverAuto
}

View File

@ -595,6 +595,19 @@ class SNMPDeviceDriverTestCase(db_base.DbTestCase):
The SNMP client object is mocked to allow various error cases to be tested.
"""
pdus = {
(1, 3, 6, 1, 4, 1, 318, 1, 1, 4): 'apc_masterswitch',
# also try longer sysObjectID
(1, 3, 6, 1, 4, 1, 318, 1, 1, 4, 1, 2, 3, 4): 'apc_masterswitch',
(1, 3, 6, 1, 4, 1, 318, 1, 1, 6): 'apc_masterswitchplus',
(1, 3, 6, 1, 4, 1, 318, 1, 1, 12): 'apc_rackpdu',
(1, 3, 6, 1, 4, 1, 21317): 'aten',
(1, 3, 6, 1, 4, 1, 3808): 'cyberpower',
(1, 3, 6, 1, 4, 1, 23620): 'teltronix',
# TODO(etingof): SNMPDriverEatonPower misses the `.oid` attribute
# and therefore fails tests
# (1, 3, 6, 1, 4, 1, 534): 'eatonpower',
}
def setUp(self):
super(SNMPDeviceDriverTestCase, self).setUp()
@ -1256,6 +1269,130 @@ class SNMPDeviceDriverTestCase(db_base.DbTestCase):
def test_teltronix_power_reset(self, mock_get_client):
self._test_simple_device_power_reset('teltronix', mock_get_client)
def test_auto_power_state_unknown_pdu(self, mock_get_client):
mock_client = mock_get_client.return_value
mock_client.get.return_value = 'unknown'
self._update_driver_info(snmp_driver="auto")
self.assertRaises(exception.InvalidParameterValue,
snmp._get_driver,
self.node)
def test_auto_power_state_pdu_discovery_failure(self, mock_get_client):
mock_client = mock_get_client.return_value
mock_client.get.side_effect = exception.SNMPFailure(operation='get',
error='')
self._update_driver_info(snmp_driver="auto")
self.assertRaises(exception.SNMPFailure, snmp._get_driver, self.node)
def test_auto_power_state_on(self, mock_get_client):
for sys_obj_oid, expected_snmp_driver in self.pdus.items():
mock_client = mock_get_client.return_value
mock_client.reset_mock()
mock_client.get.return_value = sys_obj_oid
self._update_driver_info(snmp_driver="auto")
driver = snmp._get_driver(self.node)
second_node = obj_utils.get_test_node(
self.context,
driver='fake_snmp',
driver_info=INFO_DICT)
second_node["driver_info"].update(snmp_driver=expected_snmp_driver)
second_node_driver = snmp._get_driver(second_node)
mock_client.get.return_value = second_node_driver.value_power_on
pstate = driver.power_state()
mock_client.get.assert_called_with(second_node_driver.oid)
self.assertEqual(states.POWER_ON, pstate)
def test_auto_power_state_off(self, mock_get_client):
for sys_obj_oid, expected_snmp_driver in self.pdus.items():
mock_client = mock_get_client.return_value
mock_client.reset_mock()
mock_client.get.return_value = sys_obj_oid
self._update_driver_info(snmp_driver="auto")
driver = snmp._get_driver(self.node)
second_node = obj_utils.get_test_node(
self.context,
driver='fake_snmp',
driver_info=INFO_DICT)
second_node["driver_info"].update(snmp_driver=expected_snmp_driver)
second_node_driver = snmp._get_driver(second_node)
mock_client.get.return_value = second_node_driver.value_power_off
pstate = driver.power_state()
mock_client.get.assert_called_with(second_node_driver.oid)
self.assertEqual(states.POWER_OFF, pstate)
def test_auto_power_on(self, mock_get_client):
for sys_obj_oid, expected_snmp_driver in self.pdus.items():
mock_client = mock_get_client.return_value
mock_client.reset_mock()
mock_client.get.return_value = sys_obj_oid
self._update_driver_info(snmp_driver="auto")
driver = snmp._get_driver(self.node)
second_node = obj_utils.get_test_node(
self.context,
driver='fake_snmp',
driver_info=INFO_DICT)
second_node["driver_info"].update(snmp_driver=expected_snmp_driver)
second_node_driver = snmp._get_driver(second_node)
mock_client.get.return_value = second_node_driver.value_power_on
pstate = driver.power_on()
mock_client.set.assert_called_once_with(
second_node_driver.oid,
second_node_driver.value_power_on)
self.assertEqual(states.POWER_ON, pstate)
def test_auto_power_off(self, mock_get_client):
for sys_obj_oid, expected_snmp_driver in self.pdus.items():
mock_client = mock_get_client.return_value
mock_client.reset_mock()
mock_client.get.return_value = sys_obj_oid
self._update_driver_info(snmp_driver="auto")
driver = snmp._get_driver(self.node)
second_node = obj_utils.get_test_node(
self.context,
driver='fake_snmp',
driver_info=INFO_DICT)
second_node["driver_info"].update(snmp_driver=expected_snmp_driver)
second_node_driver = snmp._get_driver(second_node)
mock_client.get.return_value = second_node_driver.value_power_off
pstate = driver.power_off()
mock_client.set.assert_called_once_with(
second_node_driver.oid,
second_node_driver.value_power_off)
self.assertEqual(states.POWER_OFF, pstate)
def test_auto_power_reset(self, mock_get_client):
for sys_obj_oid, expected_snmp_driver in self.pdus.items():
mock_client = mock_get_client.return_value
mock_client.reset_mock()
mock_client.get.side_effect = [sys_obj_oid, sys_obj_oid]
self._update_driver_info(snmp_driver="auto")
driver = snmp._get_driver(self.node)
second_node = obj_utils.get_test_node(
self.context,
driver='fake_snmp',
driver_info=INFO_DICT)
second_node["driver_info"].update(snmp_driver=expected_snmp_driver)
second_node_driver = snmp._get_driver(second_node)
mock_client.get.side_effect = [second_node_driver.value_power_off,
second_node_driver.value_power_on]
pstate = driver.power_reset()
calls = [mock.call(second_node_driver.oid,
second_node_driver.value_power_off),
mock.call(second_node_driver.oid,
second_node_driver.value_power_on)]
mock_client.set.assert_has_calls(calls)
self.assertEqual(states.POWER_ON, pstate)
def test_eaton_power_snmp_objects(self, mock_get_client):
# Ensure the correct SNMP object OIDs and values are used by the Eaton
# Power driver

View File

@ -0,0 +1,7 @@
---
features:
- |
Adds new ``auto`` type of the ``driver_info/snmp_driver`` setting which
makes ironic automatically select a suitable SNMP driver type based on
the ``SNMPv2-MIB::sysObjectID`` value as reported by the PDU being
managed.