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 The following property values have to be added to the node's
``driver_info`` field: ``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_address``: the IPv4 address of the PDU controlling this node.
- ``snmp_port``: (optional) A non-standard UDP port to use for SNMP operations. - ``snmp_port``: (optional) A non-standard UDP port to use for SNMP operations.
If not specified, the default port (161) is used. 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 1.3.6.1.4.1.21317.1.3.2.2.2.2 Outlet Power
Values: 1=Off, 2=On, 3=Pending, 4=Reset Values: 1=Off, 2=On, 3=Pending, 4=Reset
""" """
system_id = (21317,)
oid_device = (21317, 1, 3, 2, 2, 2, 2) oid_device = (21317, 1, 3, 2, 2, 2, 2)
value_power_on = 2 value_power_on = 2
value_power_off = 1 value_power_off = 1
@ -548,6 +549,7 @@ class SNMPDriverAPCMasterSwitch(SNMPDriverSimple):
Values: 1=On, 2=Off, 3=PowerCycle, [...more options follow] 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) oid_device = (318, 1, 1, 4, 4, 2, 1, 3)
value_power_on = 1 value_power_on = 1
value_power_off = 2 value_power_off = 2
@ -561,6 +563,7 @@ class SNMPDriverAPCMasterSwitchPlus(SNMPDriverSimple):
Values: 1=On, 3=Off, [...more options follow] Values: 1=On, 3=Off, [...more options follow]
""" """
system_id = (318, 1, 1, 6)
oid_device = (318, 1, 1, 6, 5, 1, 1, 5) oid_device = (318, 1, 1, 6, 5, 1, 1, 5)
value_power_on = 1 value_power_on = 1
value_power_off = 3 value_power_off = 3
@ -574,6 +577,7 @@ class SNMPDriverAPCRackPDU(SNMPDriverSimple):
Values: 1=On, 2=Off, 3=PowerCycle, [...more options follow] 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) oid_device = (318, 1, 1, 12, 3, 3, 1, 1, 4)
value_power_on = 1 value_power_on = 1
value_power_off = 2 value_power_off = 2
@ -591,6 +595,7 @@ class SNMPDriverCyberPower(SNMPDriverSimple):
# been implemented based upon its published MIB # been implemented based upon its published MIB
# documentation. # documentation.
system_id = (3808,)
oid_device = (3808, 1, 1, 3, 3, 3, 1, 1, 4) oid_device = (3808, 1, 1, 3, 3, 3, 1, 1, 4)
value_power_on = 1 value_power_on = 1
value_power_off = 2 value_power_off = 2
@ -604,6 +609,7 @@ class SNMPDriverTeltronix(SNMPDriverSimple):
Values: 1=Off, 2=On Values: 1=Off, 2=On
""" """
system_id = (23620,)
oid_device = (23620, 1, 2, 2, 1, 4) oid_device = (23620, 1, 2, 2, 1, 4)
value_power_on = 2 value_power_on = 2
value_power_off = 1 value_power_off = 1
@ -628,6 +634,7 @@ class SNMPDriverEatonPower(SNMPDriverBase):
# been implemented based upon its published MIB # been implemented based upon its published MIB
# documentation. # documentation.
system_id = (534,)
oid_device = (534, 6, 6, 7, 6, 6, 1) oid_device = (534, 6, 6, 7, 6, 6, 1)
oid_status = (2,) oid_status = (2,)
oid_poweron = (3,) oid_poweron = (3,)
@ -688,6 +695,66 @@ class SNMPDriverEatonPower(SNMPDriverBase):
self.client.set(oid, value) 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 # A dictionary of supported drivers keyed by snmp_driver attribute
DRIVER_CLASSES = { DRIVER_CLASSES = {
'apc': SNMPDriverAPCMasterSwitch, 'apc': SNMPDriverAPCMasterSwitch,
@ -697,7 +764,8 @@ DRIVER_CLASSES = {
'aten': SNMPDriverAten, 'aten': SNMPDriverAten,
'cyberpower': SNMPDriverCyberPower, 'cyberpower': SNMPDriverCyberPower,
'eatonpower': SNMPDriverEatonPower, '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. 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): def setUp(self):
super(SNMPDeviceDriverTestCase, self).setUp() super(SNMPDeviceDriverTestCase, self).setUp()
@ -1256,6 +1269,130 @@ class SNMPDeviceDriverTestCase(db_base.DbTestCase):
def test_teltronix_power_reset(self, mock_get_client): def test_teltronix_power_reset(self, mock_get_client):
self._test_simple_device_power_reset('teltronix', 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): def test_eaton_power_snmp_objects(self, mock_get_client):
# Ensure the correct SNMP object OIDs and values are used by the Eaton # Ensure the correct SNMP object OIDs and values are used by the Eaton
# Power driver # 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.