Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
From 813fb13714889a1997db52d48712dac2ab2dfb22 Mon Sep 17 00:00:00 2001
From: Nidhi Rai <nidhi.rai94@gmail.com>
Date: Thu, 11 Dec 2025 19:59:04 +0530
Subject: [PATCH] Add LLDP collect for DRAC Redfish inspection

The implementation collects LLDP data from Dell OEM SwitchConnection endpoints and falls back to standard Redfish LLDP collection when Dell specific data is unavailable.

Change-Id: If8b1ec059dbeb5a8b29f2b83b1a3e29bfa21bc1b
Signed-off-by: Nidhi Rai <nidhi.rai94@gmail.com>
---
ironic/drivers/modules/drac/inspect.py | 81 +++++++-
.../unit/drivers/modules/drac/test_inspect.py | 188 ++++++++++++++++++
2 files changed, 267 insertions(+), 2 deletions(-)

diff --git a/ironic/drivers/modules/drac/inspect.py b/ironic/drivers/modules/drac/inspect.py
index a880eeadd..001489d3b 100644
--- a/ironic/drivers/modules/drac/inspect.py
+++ b/ironic/drivers/modules/drac/inspect.py
@@ -14,12 +14,12 @@
"""
DRAC inspection interface
"""
-
from ironic.common import boot_modes
from ironic.drivers.modules.drac import utils as drac_utils
from ironic.drivers.modules import inspect_utils
from ironic.drivers.modules.redfish import inspect as redfish_inspect
from ironic.drivers.modules.redfish import utils as redfish_utils
+from oslo_log import log


_PXE_DEV_ENABLED_INTERFACES = [('PxeDev1EnDis', 'PxeDev1Interface'),
@@ -27,7 +27,7 @@ _PXE_DEV_ENABLED_INTERFACES = [('PxeDev1EnDis', 'PxeDev1Interface'),
('PxeDev3EnDis', 'PxeDev3Interface'),
('PxeDev4EnDis', 'PxeDev4Interface')]
_BIOS_ENABLED_VALUE = 'Enabled'
-
+LOG = log.getLogger(__name__)

class DracRedfishInspect(redfish_inspect.RedfishInspect):
"""iDRAC Redfish interface for inspection-related actions."""
@@ -107,3 +107,80 @@ class DracRedfishInspect(redfish_inspect.RedfishInspect):
pxe_port_macs = [mac for mac in pxe_port_macs_list]

return pxe_port_macs
+
+ def _collect_lldp_data(self, task, system):
+ """Collect LLDP data using Dell OEM SwitchConnection endpoints.
+
+ Dell iDRAC provides LLDP neighbor information through OEM
+ DellSwitchConnection endpoints. We return parsed LLDP data directly.
+
+ :param task: A TaskManager instance
+ :param system: Sushy system object
+ :returns: Dict mapping interface names to parsed LLDP data
+ """
+ parsed_lldp = {}
+
+ try:
+ # Get Dell switch connection data
+ switch_data = self._get_dell_switch_connections(task)
+
+ # Convert directly to parsed LLDP format
+ for connection in switch_data:
+ fqdd = connection.get('FQDD')
+ switch_mac = connection.get('SwitchConnectionID')
+ switch_port = connection.get('SwitchPortConnectionID')
+
+ # Skip unconnected interfaces
+ if (not fqdd or not switch_mac or not switch_port
+ or switch_mac == 'No Link' or switch_port == 'No Link'):
+ continue
+
+ parsed_lldp[fqdd] = {
+ 'switch_chassis_id': switch_mac,
+ 'switch_port_id': switch_port
+ }
+
+ LOG.debug("Generated parsed LLDP data for %d interfaces",
+ len(parsed_lldp))
+
+ except Exception as e:
+ LOG.debug("Dell OEM LLDP collection failed, falling back to "
+ "standard: %s", e)
+ # Fallback to standard Redfish LLDP collection
+ return super(DracRedfishInspect, self)._collect_lldp_data(
+ task, system)
+
+ return parsed_lldp
+
+ def _get_dell_switch_connections(self, task):
+ """Fetch Dell switch connection data via OEM.
+
+ :param task: A TaskManager instance
+ :returns: List of switch connection dictionaries
+ """
+ system = redfish_utils.get_system(task.node)
+
+ # Access Sushy's private connection object
+ try:
+ conn = system._conn
+ base_url = conn._url
+ except AttributeError as e:
+ LOG.debug("Failed to access Sushy connection object: %s", e)
+ return []
+
+ # Dell OEM endpoint for switch connections
+ # This URL structure is specific to Dell iDRAC Redfish implementation
+ switch_url = (f"{base_url}/redfish/v1/Systems/{system.identity}"
+ "/NetworkPorts/Oem/Dell/DellSwitchConnections")
+
+ LOG.debug("Fetching Dell switch connections from: %s", switch_url)
+
+ try:
+ response = conn.get(switch_url)
+ data = response.json()
+ members = data.get('Members', [])
+ LOG.debug("Retrieved %d Dell switch connections", len(members))
+ return members
+ except Exception as e:
+ LOG.debug("Failed to get Dell switch connections: %s", e)
+ return []
diff --git a/ironic/tests/unit/drivers/modules/drac/test_inspect.py b/ironic/tests/unit/drivers/modules/drac/test_inspect.py
index 41dfd8b33..4283656ad 100644
--- a/ironic/tests/unit/drivers/modules/drac/test_inspect.py
+++ b/ironic/tests/unit/drivers/modules/drac/test_inspect.py
@@ -80,6 +80,25 @@ class DracRedfishInspectionTestCase(test_utils.BaseDracTest):
]
return system_mock

+ def _setup_lldp_system_mock(self, mock_get_system):
+ """System mock for LLDP tests."""
+ system_mock = self.init_system_mock(mock_get_system.return_value)
+ system_mock.identity = 'System.Embedded.1'
+ return system_mock
+
+ def _setup_dell_connection_mock(self, system_mock, url='https://bmc.example.com/redfish/v1'):
+ """Helper to setup Dell connection mock for LLDP tests."""
+ mock_conn = mock.MagicMock()
+ mock_conn._url = url
+ system_mock._conn = mock_conn
+ return mock_conn
+
+ def _create_switch_connections_response(self, members):
+ """Create a Mock response for Dell switch connections."""
+ mock_response = mock.MagicMock()
+ mock_response.json.return_value = {'Members': members}
+ return mock_response
+
def test_get_properties(self):
expected = redfish_utils.COMMON_PROPERTIES
driver = drac_inspect.DracRedfishInspect()
@@ -158,3 +177,172 @@ class DracRedfishInspectionTestCase(test_utils.BaseDracTest):
shared=True) as task:
return_value = task.driver.inspect._get_mac_address(task)
self.assertEqual(expected_value, return_value)
+ @mock.patch.object(redfish_utils, 'get_system', autospec=True)
+ def test_collect_lldp_data_successful_dell_oem(self, mock_get_system):
+ """Test successful LLDP data collection from Dell OEM endpoints."""
+ system_mock = self._setup_lldp_system_mock(mock_get_system)
+ mock_conn = self._setup_dell_connection_mock(system_mock)
+
+ # Mock the HTTP response with switch connections
+ members = [
+ {
+ 'FQDD': 'NIC.Integrated.1-1-1',
+ 'SwitchConnectionID': 'aa:bb:cc:dd:ee:ff',
+ 'SwitchPortConnectionID': 'Ethernet1/0/1'
+ },
+ {
+ 'FQDD': 'NIC.Integrated.1-1-2',
+ 'SwitchConnectionID': 'aa:bb:cc:dd:ee:gg',
+ 'SwitchPortConnectionID': 'Ethernet1/8'
+ }
+ ]
+ mock_response = self._create_switch_connections_response(members)
+ mock_conn.get.return_value = mock_response
+
+ expected_lldp = {
+ 'NIC.Integrated.1-1-1': {
+ 'switch_chassis_id': 'aa:bb:cc:dd:ee:ff',
+ 'switch_port_id': 'Ethernet1/0/1'
+ },
+ 'NIC.Integrated.1-1-2': {
+ 'switch_chassis_id': 'aa:bb:cc:dd:ee:gg',
+ 'switch_port_id': 'Ethernet1/8'
+ }
+ }
+
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ result = task.driver.inspect._collect_lldp_data(task, system_mock)
+ self.assertEqual(expected_lldp, result)
+
+ @mock.patch.object(redfish_inspect.RedfishInspect, '_collect_lldp_data',
+ autospec=True)
+ @mock.patch.object(drac_inspect.DracRedfishInspect,
+ '_get_dell_switch_connections', autospec=True)
+ @mock.patch.object(redfish_utils, 'get_system', autospec=True)
+ def test_collect_lldp_data_fallback_to_standard(self, mock_get_system,
+ mock_get_connections,
+ mock_super_collect):
+ """Test fallback to standard Redfish LLDP when Dell OEM fails."""
+ system_mock = self._setup_lldp_system_mock(mock_get_system)
+
+ # Mock _get_dell_switch_connections to raise an exception
+ mock_get_connections.side_effect = Exception("Dell OEM failed")
+
+ # Mock fallback response
+ mock_super_collect.return_value = {
+ 'NIC.Integrated.1-1-1': {
+ 'switch_chassis_id': 'fallback_chassis',
+ 'switch_port_id': 'fallback_port'
+ }
+ }
+
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ result = task.driver.inspect._collect_lldp_data(task, system_mock)
+ # Should return the fallback data
+ mock_super_collect.assert_called_once_with(
+ task.driver.inspect, task, system_mock)
+ self.assertEqual(mock_super_collect.return_value, result)
+
+ @mock.patch.object(redfish_utils, 'get_system', autospec=True)
+ def test_collect_lldp_data_filters_no_link(self, mock_get_system):
+ """Test that 'No Link' connections are filtered out."""
+ system_mock = self._setup_lldp_system_mock(mock_get_system)
+ mock_conn = self._setup_dell_connection_mock(system_mock)
+
+ # Mock the HTTP response with mixed valid/invalid connections
+ members = [
+ {
+ 'FQDD': 'NIC.Integrated.1-1-1',
+ 'SwitchConnectionID': 'aa:bb:cc:dd:ee:ff',
+ 'SwitchPortConnectionID': 'Ethernet1/8'
+ },
+ {
+ 'FQDD': 'NIC.Integrated.1-1-2',
+ 'SwitchConnectionID': 'No Link',
+ 'SwitchPortConnectionID': 'No Link'
+ },
+ {
+ 'FQDD': None,
+ 'SwitchConnectionID': 'aa:bb:cc:dd:ee:gg',
+ 'SwitchPortConnectionID': 'Ethernet1/8'
+ }
+ ]
+ mock_response = self._create_switch_connections_response(members)
+ mock_conn.get.return_value = mock_response
+
+ expected_lldp = {
+ 'NIC.Integrated.1-1-1': {
+ 'switch_chassis_id': 'aa:bb:cc:dd:ee:ff',
+ 'switch_port_id': 'Ethernet1/8'
+ }
+ }
+
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ result = task.driver.inspect._collect_lldp_data(task, system_mock)
+ self.assertEqual(expected_lldp, result)
+
+ @mock.patch.object(redfish_utils, 'get_system', autospec=True)
+ def test_get_dell_switch_connections_success(self, mock_get_system):
+ """Test successful retrieval of Dell switch connections."""
+ system_mock = self._setup_lldp_system_mock(mock_get_system)
+ mock_conn = self._setup_dell_connection_mock(system_mock)
+
+ expected_members = [
+ {'FQDD': 'NIC.Integrated.1-1-1',
+ 'SwitchConnectionID': 'aa:bb:cc:dd:ee:ff'},
+ {'FQDD': 'NIC.Integrated.1-1-2',
+ 'SwitchConnectionID': 'aa:bb:cc:dd:ee:gg'}
+ ]
+ mock_response = self._create_switch_connections_response(
+ expected_members)
+ mock_conn.get.return_value = mock_response
+
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ result = task.driver.inspect._get_dell_switch_connections(task)
+ self.assertEqual(expected_members, result)
+
+ @mock.patch.object(redfish_utils, 'get_system', autospec=True)
+ def test_get_dell_switch_connections_attr_error(self, mock_get_system):
+ """Test AttributeError when accessing private attributes."""
+ system_mock = self._setup_lldp_system_mock(mock_get_system)
+
+ # Mock missing _conn attribute
+ delattr(system_mock, '_conn')
+
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ result = task.driver.inspect._get_dell_switch_connections(task)
+ self.assertEqual([], result)
+
+ @mock.patch.object(redfish_utils, 'get_system', autospec=True)
+ def test_get_dell_switch_connections_conn_error(self, mock_get_system):
+ """Test handling of connection errors during HTTP request."""
+ system_mock = self._setup_lldp_system_mock(mock_get_system)
+ mock_conn = self._setup_dell_connection_mock(system_mock)
+
+ # Mock connection failure
+ mock_conn.get.side_effect = Exception("HTTP connection failed")
+
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ result = task.driver.inspect._get_dell_switch_connections(task)
+ self.assertEqual([], result)
+
+ @mock.patch.object(redfish_utils, 'get_system', autospec=True)
+ def test_get_dell_switch_connections_empty_response(self, mock_get_system):
+ """Test handling of empty response from Dell OEM endpoint."""
+ system_mock = self._setup_lldp_system_mock(mock_get_system)
+ mock_conn = self._setup_dell_connection_mock(system_mock)
+
+ # Mock empty response
+ mock_response = self._create_switch_connections_response([])
+ mock_conn.get.return_value = mock_response
+
+ with task_manager.acquire(self.context, self.node.uuid,
+ shared=True) as task:
+ result = task.driver.inspect._get_dell_switch_connections(task)
+ self.assertEqual([], result)
--
2.50.1 (Apple Git-155)
Loading
Loading