diff --git a/doc/source/admin/drivers/snmp.rst b/doc/source/admin/drivers/snmp.rst index f71a9b241f..92a730f406 100644 --- a/doc/source/admin/drivers/snmp.rst +++ b/doc/source/admin/drivers/snmp.rst @@ -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. diff --git a/ironic/drivers/modules/snmp.py b/ironic/drivers/modules/snmp.py index a67ca2ffbf..8708aeafce 100644 --- a/ironic/drivers/modules/snmp.py +++ b/ironic/drivers/modules/snmp.py @@ -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 } diff --git a/ironic/tests/unit/drivers/modules/test_snmp.py b/ironic/tests/unit/drivers/modules/test_snmp.py index 51a8701bb5..e31f2e15ee 100644 --- a/ironic/tests/unit/drivers/modules/test_snmp.py +++ b/ironic/tests/unit/drivers/modules/test_snmp.py @@ -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 diff --git a/releasenotes/notes/add-snmp-pdu-driver-type-discovery-1f280b7f06fd1ca5.yaml b/releasenotes/notes/add-snmp-pdu-driver-type-discovery-1f280b7f06fd1ca5.yaml new file mode 100644 index 0000000000..7086d25090 --- /dev/null +++ b/releasenotes/notes/add-snmp-pdu-driver-type-discovery-1f280b7f06fd1ca5.yaml @@ -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.