Skip to content

Commit e41c8d0

Browse files
committed
feat(ironic): backport Dell LLDP support and redfish interface fixes
Backported the Dell LLDP support and the redfish interfaces cleanup to filter out interfaces without MAC addresses.
1 parent b8edacd commit e41c8d0

File tree

3 files changed

+444
-0
lines changed

3 files changed

+444
-0
lines changed
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
From 813fb13714889a1997db52d48712dac2ab2dfb22 Mon Sep 17 00:00:00 2001
2+
From: Nidhi Rai <[email protected]>
3+
Date: Thu, 11 Dec 2025 19:59:04 +0530
4+
Subject: [PATCH] Add LLDP collect for DRAC Redfish inspection
5+
6+
The implementation collects LLDP data from Dell OEM SwitchConnection endpoints and falls back to standard Redfish LLDP collection when Dell specific data is unavailable.
7+
8+
Change-Id: If8b1ec059dbeb5a8b29f2b83b1a3e29bfa21bc1b
9+
Signed-off-by: Nidhi Rai <[email protected]>
10+
---
11+
ironic/drivers/modules/drac/inspect.py | 81 +++++++-
12+
.../unit/drivers/modules/drac/test_inspect.py | 188 ++++++++++++++++++
13+
2 files changed, 267 insertions(+), 2 deletions(-)
14+
15+
diff --git a/ironic/drivers/modules/drac/inspect.py b/ironic/drivers/modules/drac/inspect.py
16+
index a880eeadd..001489d3b 100644
17+
--- a/ironic/drivers/modules/drac/inspect.py
18+
+++ b/ironic/drivers/modules/drac/inspect.py
19+
@@ -14,12 +14,12 @@
20+
"""
21+
DRAC inspection interface
22+
"""
23+
-
24+
from ironic.common import boot_modes
25+
from ironic.drivers.modules.drac import utils as drac_utils
26+
from ironic.drivers.modules import inspect_utils
27+
from ironic.drivers.modules.redfish import inspect as redfish_inspect
28+
from ironic.drivers.modules.redfish import utils as redfish_utils
29+
+from oslo_log import log
30+
31+
32+
_PXE_DEV_ENABLED_INTERFACES = [('PxeDev1EnDis', 'PxeDev1Interface'),
33+
@@ -27,7 +27,7 @@ _PXE_DEV_ENABLED_INTERFACES = [('PxeDev1EnDis', 'PxeDev1Interface'),
34+
('PxeDev3EnDis', 'PxeDev3Interface'),
35+
('PxeDev4EnDis', 'PxeDev4Interface')]
36+
_BIOS_ENABLED_VALUE = 'Enabled'
37+
-
38+
+LOG = log.getLogger(__name__)
39+
40+
class DracRedfishInspect(redfish_inspect.RedfishInspect):
41+
"""iDRAC Redfish interface for inspection-related actions."""
42+
@@ -107,3 +107,80 @@ class DracRedfishInspect(redfish_inspect.RedfishInspect):
43+
pxe_port_macs = [mac for mac in pxe_port_macs_list]
44+
45+
return pxe_port_macs
46+
+
47+
+ def _collect_lldp_data(self, task, system):
48+
+ """Collect LLDP data using Dell OEM SwitchConnection endpoints.
49+
+
50+
+ Dell iDRAC provides LLDP neighbor information through OEM
51+
+ DellSwitchConnection endpoints. We return parsed LLDP data directly.
52+
+
53+
+ :param task: A TaskManager instance
54+
+ :param system: Sushy system object
55+
+ :returns: Dict mapping interface names to parsed LLDP data
56+
+ """
57+
+ parsed_lldp = {}
58+
+
59+
+ try:
60+
+ # Get Dell switch connection data
61+
+ switch_data = self._get_dell_switch_connections(task)
62+
+
63+
+ # Convert directly to parsed LLDP format
64+
+ for connection in switch_data:
65+
+ fqdd = connection.get('FQDD')
66+
+ switch_mac = connection.get('SwitchConnectionID')
67+
+ switch_port = connection.get('SwitchPortConnectionID')
68+
+
69+
+ # Skip unconnected interfaces
70+
+ if (not fqdd or not switch_mac or not switch_port
71+
+ or switch_mac == 'No Link' or switch_port == 'No Link'):
72+
+ continue
73+
+
74+
+ parsed_lldp[fqdd] = {
75+
+ 'switch_chassis_id': switch_mac,
76+
+ 'switch_port_id': switch_port
77+
+ }
78+
+
79+
+ LOG.debug("Generated parsed LLDP data for %d interfaces",
80+
+ len(parsed_lldp))
81+
+
82+
+ except Exception as e:
83+
+ LOG.debug("Dell OEM LLDP collection failed, falling back to "
84+
+ "standard: %s", e)
85+
+ # Fallback to standard Redfish LLDP collection
86+
+ return super(DracRedfishInspect, self)._collect_lldp_data(
87+
+ task, system)
88+
+
89+
+ return parsed_lldp
90+
+
91+
+ def _get_dell_switch_connections(self, task):
92+
+ """Fetch Dell switch connection data via OEM.
93+
+
94+
+ :param task: A TaskManager instance
95+
+ :returns: List of switch connection dictionaries
96+
+ """
97+
+ system = redfish_utils.get_system(task.node)
98+
+
99+
+ # Access Sushy's private connection object
100+
+ try:
101+
+ conn = system._conn
102+
+ base_url = conn._url
103+
+ except AttributeError as e:
104+
+ LOG.debug("Failed to access Sushy connection object: %s", e)
105+
+ return []
106+
+
107+
+ # Dell OEM endpoint for switch connections
108+
+ # This URL structure is specific to Dell iDRAC Redfish implementation
109+
+ switch_url = (f"{base_url}/redfish/v1/Systems/{system.identity}"
110+
+ "/NetworkPorts/Oem/Dell/DellSwitchConnections")
111+
+
112+
+ LOG.debug("Fetching Dell switch connections from: %s", switch_url)
113+
+
114+
+ try:
115+
+ response = conn.get(switch_url)
116+
+ data = response.json()
117+
+ members = data.get('Members', [])
118+
+ LOG.debug("Retrieved %d Dell switch connections", len(members))
119+
+ return members
120+
+ except Exception as e:
121+
+ LOG.debug("Failed to get Dell switch connections: %s", e)
122+
+ return []
123+
diff --git a/ironic/tests/unit/drivers/modules/drac/test_inspect.py b/ironic/tests/unit/drivers/modules/drac/test_inspect.py
124+
index 41dfd8b33..4283656ad 100644
125+
--- a/ironic/tests/unit/drivers/modules/drac/test_inspect.py
126+
+++ b/ironic/tests/unit/drivers/modules/drac/test_inspect.py
127+
@@ -80,6 +80,25 @@ class DracRedfishInspectionTestCase(test_utils.BaseDracTest):
128+
]
129+
return system_mock
130+
131+
+ def _setup_lldp_system_mock(self, mock_get_system):
132+
+ """System mock for LLDP tests."""
133+
+ system_mock = self.init_system_mock(mock_get_system.return_value)
134+
+ system_mock.identity = 'System.Embedded.1'
135+
+ return system_mock
136+
+
137+
+ def _setup_dell_connection_mock(self, system_mock, url='https://bmc.example.com/redfish/v1'):
138+
+ """Helper to setup Dell connection mock for LLDP tests."""
139+
+ mock_conn = mock.MagicMock()
140+
+ mock_conn._url = url
141+
+ system_mock._conn = mock_conn
142+
+ return mock_conn
143+
+
144+
+ def _create_switch_connections_response(self, members):
145+
+ """Create a Mock response for Dell switch connections."""
146+
+ mock_response = mock.MagicMock()
147+
+ mock_response.json.return_value = {'Members': members}
148+
+ return mock_response
149+
+
150+
def test_get_properties(self):
151+
expected = redfish_utils.COMMON_PROPERTIES
152+
driver = drac_inspect.DracRedfishInspect()
153+
@@ -158,3 +177,172 @@ class DracRedfishInspectionTestCase(test_utils.BaseDracTest):
154+
shared=True) as task:
155+
return_value = task.driver.inspect._get_mac_address(task)
156+
self.assertEqual(expected_value, return_value)
157+
+ @mock.patch.object(redfish_utils, 'get_system', autospec=True)
158+
+ def test_collect_lldp_data_successful_dell_oem(self, mock_get_system):
159+
+ """Test successful LLDP data collection from Dell OEM endpoints."""
160+
+ system_mock = self._setup_lldp_system_mock(mock_get_system)
161+
+ mock_conn = self._setup_dell_connection_mock(system_mock)
162+
+
163+
+ # Mock the HTTP response with switch connections
164+
+ members = [
165+
+ {
166+
+ 'FQDD': 'NIC.Integrated.1-1-1',
167+
+ 'SwitchConnectionID': 'aa:bb:cc:dd:ee:ff',
168+
+ 'SwitchPortConnectionID': 'Ethernet1/0/1'
169+
+ },
170+
+ {
171+
+ 'FQDD': 'NIC.Integrated.1-1-2',
172+
+ 'SwitchConnectionID': 'aa:bb:cc:dd:ee:gg',
173+
+ 'SwitchPortConnectionID': 'Ethernet1/8'
174+
+ }
175+
+ ]
176+
+ mock_response = self._create_switch_connections_response(members)
177+
+ mock_conn.get.return_value = mock_response
178+
+
179+
+ expected_lldp = {
180+
+ 'NIC.Integrated.1-1-1': {
181+
+ 'switch_chassis_id': 'aa:bb:cc:dd:ee:ff',
182+
+ 'switch_port_id': 'Ethernet1/0/1'
183+
+ },
184+
+ 'NIC.Integrated.1-1-2': {
185+
+ 'switch_chassis_id': 'aa:bb:cc:dd:ee:gg',
186+
+ 'switch_port_id': 'Ethernet1/8'
187+
+ }
188+
+ }
189+
+
190+
+ with task_manager.acquire(self.context, self.node.uuid,
191+
+ shared=True) as task:
192+
+ result = task.driver.inspect._collect_lldp_data(task, system_mock)
193+
+ self.assertEqual(expected_lldp, result)
194+
+
195+
+ @mock.patch.object(redfish_inspect.RedfishInspect, '_collect_lldp_data',
196+
+ autospec=True)
197+
+ @mock.patch.object(drac_inspect.DracRedfishInspect,
198+
+ '_get_dell_switch_connections', autospec=True)
199+
+ @mock.patch.object(redfish_utils, 'get_system', autospec=True)
200+
+ def test_collect_lldp_data_fallback_to_standard(self, mock_get_system,
201+
+ mock_get_connections,
202+
+ mock_super_collect):
203+
+ """Test fallback to standard Redfish LLDP when Dell OEM fails."""
204+
+ system_mock = self._setup_lldp_system_mock(mock_get_system)
205+
+
206+
+ # Mock _get_dell_switch_connections to raise an exception
207+
+ mock_get_connections.side_effect = Exception("Dell OEM failed")
208+
+
209+
+ # Mock fallback response
210+
+ mock_super_collect.return_value = {
211+
+ 'NIC.Integrated.1-1-1': {
212+
+ 'switch_chassis_id': 'fallback_chassis',
213+
+ 'switch_port_id': 'fallback_port'
214+
+ }
215+
+ }
216+
+
217+
+ with task_manager.acquire(self.context, self.node.uuid,
218+
+ shared=True) as task:
219+
+ result = task.driver.inspect._collect_lldp_data(task, system_mock)
220+
+ # Should return the fallback data
221+
+ mock_super_collect.assert_called_once_with(
222+
+ task.driver.inspect, task, system_mock)
223+
+ self.assertEqual(mock_super_collect.return_value, result)
224+
+
225+
+ @mock.patch.object(redfish_utils, 'get_system', autospec=True)
226+
+ def test_collect_lldp_data_filters_no_link(self, mock_get_system):
227+
+ """Test that 'No Link' connections are filtered out."""
228+
+ system_mock = self._setup_lldp_system_mock(mock_get_system)
229+
+ mock_conn = self._setup_dell_connection_mock(system_mock)
230+
+
231+
+ # Mock the HTTP response with mixed valid/invalid connections
232+
+ members = [
233+
+ {
234+
+ 'FQDD': 'NIC.Integrated.1-1-1',
235+
+ 'SwitchConnectionID': 'aa:bb:cc:dd:ee:ff',
236+
+ 'SwitchPortConnectionID': 'Ethernet1/8'
237+
+ },
238+
+ {
239+
+ 'FQDD': 'NIC.Integrated.1-1-2',
240+
+ 'SwitchConnectionID': 'No Link',
241+
+ 'SwitchPortConnectionID': 'No Link'
242+
+ },
243+
+ {
244+
+ 'FQDD': None,
245+
+ 'SwitchConnectionID': 'aa:bb:cc:dd:ee:gg',
246+
+ 'SwitchPortConnectionID': 'Ethernet1/8'
247+
+ }
248+
+ ]
249+
+ mock_response = self._create_switch_connections_response(members)
250+
+ mock_conn.get.return_value = mock_response
251+
+
252+
+ expected_lldp = {
253+
+ 'NIC.Integrated.1-1-1': {
254+
+ 'switch_chassis_id': 'aa:bb:cc:dd:ee:ff',
255+
+ 'switch_port_id': 'Ethernet1/8'
256+
+ }
257+
+ }
258+
+
259+
+ with task_manager.acquire(self.context, self.node.uuid,
260+
+ shared=True) as task:
261+
+ result = task.driver.inspect._collect_lldp_data(task, system_mock)
262+
+ self.assertEqual(expected_lldp, result)
263+
+
264+
+ @mock.patch.object(redfish_utils, 'get_system', autospec=True)
265+
+ def test_get_dell_switch_connections_success(self, mock_get_system):
266+
+ """Test successful retrieval of Dell switch connections."""
267+
+ system_mock = self._setup_lldp_system_mock(mock_get_system)
268+
+ mock_conn = self._setup_dell_connection_mock(system_mock)
269+
+
270+
+ expected_members = [
271+
+ {'FQDD': 'NIC.Integrated.1-1-1',
272+
+ 'SwitchConnectionID': 'aa:bb:cc:dd:ee:ff'},
273+
+ {'FQDD': 'NIC.Integrated.1-1-2',
274+
+ 'SwitchConnectionID': 'aa:bb:cc:dd:ee:gg'}
275+
+ ]
276+
+ mock_response = self._create_switch_connections_response(
277+
+ expected_members)
278+
+ mock_conn.get.return_value = mock_response
279+
+
280+
+ with task_manager.acquire(self.context, self.node.uuid,
281+
+ shared=True) as task:
282+
+ result = task.driver.inspect._get_dell_switch_connections(task)
283+
+ self.assertEqual(expected_members, result)
284+
+
285+
+ @mock.patch.object(redfish_utils, 'get_system', autospec=True)
286+
+ def test_get_dell_switch_connections_attr_error(self, mock_get_system):
287+
+ """Test AttributeError when accessing private attributes."""
288+
+ system_mock = self._setup_lldp_system_mock(mock_get_system)
289+
+
290+
+ # Mock missing _conn attribute
291+
+ delattr(system_mock, '_conn')
292+
+
293+
+ with task_manager.acquire(self.context, self.node.uuid,
294+
+ shared=True) as task:
295+
+ result = task.driver.inspect._get_dell_switch_connections(task)
296+
+ self.assertEqual([], result)
297+
+
298+
+ @mock.patch.object(redfish_utils, 'get_system', autospec=True)
299+
+ def test_get_dell_switch_connections_conn_error(self, mock_get_system):
300+
+ """Test handling of connection errors during HTTP request."""
301+
+ system_mock = self._setup_lldp_system_mock(mock_get_system)
302+
+ mock_conn = self._setup_dell_connection_mock(system_mock)
303+
+
304+
+ # Mock connection failure
305+
+ mock_conn.get.side_effect = Exception("HTTP connection failed")
306+
+
307+
+ with task_manager.acquire(self.context, self.node.uuid,
308+
+ shared=True) as task:
309+
+ result = task.driver.inspect._get_dell_switch_connections(task)
310+
+ self.assertEqual([], result)
311+
+
312+
+ @mock.patch.object(redfish_utils, 'get_system', autospec=True)
313+
+ def test_get_dell_switch_connections_empty_response(self, mock_get_system):
314+
+ """Test handling of empty response from Dell OEM endpoint."""
315+
+ system_mock = self._setup_lldp_system_mock(mock_get_system)
316+
+ mock_conn = self._setup_dell_connection_mock(system_mock)
317+
+
318+
+ # Mock empty response
319+
+ mock_response = self._create_switch_connections_response([])
320+
+ mock_conn.get.return_value = mock_response
321+
+
322+
+ with task_manager.acquire(self.context, self.node.uuid,
323+
+ shared=True) as task:
324+
+ result = task.driver.inspect._get_dell_switch_connections(task)
325+
+ self.assertEqual([], result)
326+
--
327+
2.50.1 (Apple Git-155)

0 commit comments

Comments
 (0)