|
| 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