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:
parent
04a5ef1d39
commit
80d6c14db7
@ -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.
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user