mirror of
https://opendev.org/openstack/ironic.git
synced 2026-01-11 19:57:20 +00:00
Add Redfish LLDP data collection support to the Redfish inspection interface.
- _collect_lldp_data(): Collects LLDP data from Redfish NetworkAdapter Ports via Sushy library, walking the Chassis/NetworkAdapter/Port hierarchy - Integration with inspect_hardware(): LLDP collection is called during hardware inspection and results are stored in plugin_data['parsed_lldp'] The implementation supports standard Redfish LLDP data from Port.Ethernet.LLDPReceive fields and can be extended by vendor-specific implementations (like, Dell DRAC OEM endpoints) through method overriding. Change-Id: I25889b2a2eb8f6a2d796dfbeb598875a7c07b22c Signed-off-by: Nidhi Rai <nidhi.rai94@gmail.com>
This commit is contained in:
parent
530c88757a
commit
e273bb958a
4 changed files with 352 additions and 1 deletions
|
|
@ -386,7 +386,10 @@ Out-Of-Band inspection
|
|||
The ``redfish`` hardware type can inspect the bare metal node by querying
|
||||
Redfish compatible BMC. This process is quick and reliable compared to the
|
||||
way the ``agent`` hardware type works i.e. booting bare metal node into
|
||||
the introspection ramdisk.
|
||||
the introspection ramdisk. The inspection collects various hardware
|
||||
information including LLDP (Link Layer Discovery Protocol) data when
|
||||
available from the BMC, such as chassis ID, port ID, system name, system
|
||||
description, system capabilities, and management addresses.
|
||||
|
||||
.. note::
|
||||
|
||||
|
|
|
|||
|
|
@ -190,6 +190,15 @@ class RedfishInspect(base.InspectInterface):
|
|||
|
||||
plugin_data = {}
|
||||
|
||||
# Collect LLDP data from Redfish NetworkAdapter Ports
|
||||
# This method can be overridden by vendor-specific implementations
|
||||
lldp_raw_data = self._collect_lldp_data(task, system)
|
||||
if lldp_raw_data:
|
||||
plugin_data['parsed_lldp'] = lldp_raw_data
|
||||
LOG.info('Collected LLDP data for %(count)d interface(s) on '
|
||||
'node %(node)s',
|
||||
{'count': len(lldp_raw_data), 'node': task.node.uuid})
|
||||
|
||||
inspect_utils.run_inspection_hooks(task, inventory, plugin_data,
|
||||
self.hooks, None)
|
||||
inspect_utils.store_inspection_data(task.node,
|
||||
|
|
@ -373,3 +382,136 @@ class RedfishInspect(base.InspectInterface):
|
|||
if function.revision_id is not None:
|
||||
info['revision'] = function.revision_id
|
||||
return info
|
||||
|
||||
def _collect_lldp_data(self, task, system):
|
||||
"""Collect LLDP data from Redfish NetworkAdapter Ports.
|
||||
|
||||
This method can be overridden by vendor-specific implementations
|
||||
to provide alternative LLDP data sources (e.g., Dell OEM endpoints).
|
||||
|
||||
Default implementation uses standard Redfish LLDP data from
|
||||
Port.Ethernet.LLDPReceive via Sushy NetworkAdapter/Port resources.
|
||||
|
||||
:param task: A TaskManager instance
|
||||
:param system: Sushy system object
|
||||
:returns: Dict mapping interface names to parsed LLDP data
|
||||
Format: {'interface_name': {'switch_chassis_id': '..',
|
||||
'switch_port_id': '..'}}
|
||||
"""
|
||||
parsed_lldp = {}
|
||||
|
||||
try:
|
||||
# Check if chassis exists
|
||||
if not system.chassis:
|
||||
return parsed_lldp
|
||||
|
||||
# Process each chassis
|
||||
for chassis in system.chassis:
|
||||
try:
|
||||
# Get NetworkAdapters collection
|
||||
network_adapters = (
|
||||
chassis.network_adapters.get_members())
|
||||
except sushy.exceptions.SushyError as ex:
|
||||
LOG.debug('Failed to get network adapters for chassis '
|
||||
'on node %(node)s: %(error)s',
|
||||
{'node': task.node.uuid, 'error': ex})
|
||||
continue
|
||||
|
||||
# Process each NetworkAdapter
|
||||
for adapter in network_adapters:
|
||||
try:
|
||||
# Get Ports collection using Sushy
|
||||
ports = adapter.ports.get_members()
|
||||
except sushy.exceptions.SushyError as ex:
|
||||
LOG.debug('Failed to get ports for adapter '
|
||||
'on node %(node)s: %(error)s',
|
||||
{'node': task.node.uuid, 'error': ex})
|
||||
continue
|
||||
|
||||
# Process each Port
|
||||
for port in ports:
|
||||
try:
|
||||
# Check if LLDP data exists using Sushy
|
||||
if (not port.ethernet
|
||||
or not port.ethernet.lldp_receive):
|
||||
continue
|
||||
|
||||
lldp_receive = port.ethernet.lldp_receive
|
||||
|
||||
# Convert directly to parsed LLDP format
|
||||
lldp_dict = self._convert_lldp_receive_to_dict(
|
||||
lldp_receive)
|
||||
|
||||
if not lldp_dict:
|
||||
continue
|
||||
|
||||
# Use port identity directly as interface name
|
||||
if port.identity:
|
||||
parsed_lldp[port.identity] = lldp_dict
|
||||
|
||||
except Exception as e:
|
||||
LOG.debug('Failed to process LLDP data for port '
|
||||
'%(port)s on node %(node)s: %(error)s',
|
||||
{'port': port.identity,
|
||||
'node': task.node.uuid, 'error': e})
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
LOG.warning('Failed to collect standard Redfish LLDP data for '
|
||||
'node %(node)s: %(error)s',
|
||||
{'node': task.node.uuid, 'error': e})
|
||||
return parsed_lldp
|
||||
|
||||
def _convert_lldp_receive_to_dict(self, lldp_receive):
|
||||
"""Convert Sushy LLDPReceive object directly to parsed dict format.
|
||||
|
||||
:param lldp_receive: Sushy LLDPReceiveField object or dict
|
||||
:returns: Dict with parsed LLDP data or None
|
||||
"""
|
||||
lldp_dict = {}
|
||||
|
||||
# Chassis ID
|
||||
chassis_id = self._get_lldp_value(lldp_receive, 'chassis_id',
|
||||
'ChassisId')
|
||||
if chassis_id:
|
||||
lldp_dict['switch_chassis_id'] = chassis_id
|
||||
|
||||
# Port ID
|
||||
port_id = self._get_lldp_value(lldp_receive, 'port_id', 'PortId')
|
||||
if port_id:
|
||||
lldp_dict['switch_port_id'] = port_id
|
||||
|
||||
# System Name
|
||||
system_name = self._get_lldp_value(lldp_receive, 'system_name',
|
||||
'SystemName')
|
||||
if system_name:
|
||||
lldp_dict['switch_system_name'] = system_name
|
||||
|
||||
# System Description
|
||||
system_description = self._get_lldp_value(lldp_receive,
|
||||
'system_description',
|
||||
'SystemDescription')
|
||||
if system_description:
|
||||
lldp_dict['switch_system_description'] = system_description
|
||||
|
||||
# Management VLAN ID
|
||||
vlan_id = self._get_lldp_value(lldp_receive, 'management_vlan_id',
|
||||
'ManagementVlanId')
|
||||
if vlan_id:
|
||||
lldp_dict['switch_vlan_id'] = vlan_id
|
||||
|
||||
return lldp_dict if lldp_dict else None
|
||||
|
||||
def _get_lldp_value(self, lldp_receive, attr_name, json_key):
|
||||
"""Get value from LLDP receive, handling both dict and object.
|
||||
|
||||
:param lldp_receive: LLDP data (Sushy object or dict)
|
||||
:param attr_name: Sushy attribute name
|
||||
:param json_key: JSON property name (required)
|
||||
:returns: The value or None
|
||||
"""
|
||||
# Being defensive to handle both Sushy object and dict
|
||||
if isinstance(lldp_receive, dict):
|
||||
return lldp_receive.get(json_key)
|
||||
else:
|
||||
return getattr(lldp_receive, attr_name, None)
|
||||
|
|
|
|||
|
|
@ -858,6 +858,203 @@ class RedfishInspectTestCase(db_base.DbTestCase):
|
|||
self.assertEqual(expected_pcie_devices,
|
||||
inventory['inventory']['pci_devices'])
|
||||
|
||||
@mock.patch.object(redfish_utils, 'get_enabled_macs', autospec=True)
|
||||
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
|
||||
def test_collect_lldp_data_with_complete_lldp(
|
||||
self, mock_get_system, mock_get_enabled_macs):
|
||||
"""Test LLDP collection with complete LLDP data"""
|
||||
system_mock = self.init_system_mock(mock_get_system.return_value)
|
||||
|
||||
# Mock NetworkAdapters and Ports with LLDP data
|
||||
mock_chassis = mock.Mock()
|
||||
mock_chassis.identity = 'System.Embedded.1'
|
||||
|
||||
# Mock Port with complete LLDP data
|
||||
mock_lldp = mock.Mock()
|
||||
mock_lldp.chassis_id = 'c4:7e:e0:e4:55:3f'
|
||||
mock_lldp.chassis_id_subtype = mock.Mock(value='MacAddr')
|
||||
mock_lldp.port_id = 'Ethernet1/8'
|
||||
mock_lldp.port_id_subtype = mock.Mock(value='IfName')
|
||||
mock_lldp.system_name = 'switch-01.example.com'
|
||||
mock_lldp.system_description = 'Cisco IOS XE'
|
||||
mock_lldp.system_capabilities = [
|
||||
mock.Mock(value='Bridge'),
|
||||
mock.Mock(value='Router')]
|
||||
mock_lldp.management_address_ipv4 = '192.168.1.1'
|
||||
mock_lldp.management_address_ipv6 = None
|
||||
mock_lldp.management_address_mac = None
|
||||
mock_lldp.management_vlan_id = 100
|
||||
|
||||
mock_ethernet = mock.Mock()
|
||||
mock_ethernet.lldp_receive = mock_lldp
|
||||
mock_ethernet.associated_mac_addresses = ['14:23:F3:F5:3B:A0']
|
||||
|
||||
mock_port = mock.Mock()
|
||||
mock_port.identity = 'NIC.Slot.1-1'
|
||||
mock_port.ethernet = mock_ethernet
|
||||
|
||||
mock_adapter = mock.Mock()
|
||||
mock_adapter.identity = 'NIC.Slot.1'
|
||||
mock_adapter.ports.get_members.return_value = [mock_port]
|
||||
|
||||
mock_chassis.network_adapters.get_members.return_value = [mock_adapter]
|
||||
system_mock.chassis = [mock_chassis]
|
||||
|
||||
# Mock ethernet interface for mapping
|
||||
mock_iface = mock.Mock()
|
||||
mock_iface.identity = 'NIC.Slot.1-1-1'
|
||||
mock_iface.mac_address = '14:23:F3:F5:3B:A0'
|
||||
system_mock.ethernet_interfaces.get_members.return_value = [mock_iface]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
task.driver.inspect.inspect_hardware(task)
|
||||
|
||||
inventory = inspect_utils.get_inspection_data(self.node, self.context)
|
||||
|
||||
# Verify parsed_lldp was collected
|
||||
self.assertIn('parsed_lldp', inventory['plugin_data'])
|
||||
parsed_lldp = inventory['plugin_data']['parsed_lldp']
|
||||
|
||||
# Should have one interface with LLDP data using Port ID as name
|
||||
self.assertEqual(1, len(parsed_lldp))
|
||||
self.assertIn('NIC.Slot.1-1', parsed_lldp)
|
||||
|
||||
# Verify parsed LLDP format
|
||||
lldp_data = parsed_lldp['NIC.Slot.1-1']
|
||||
self.assertIsInstance(lldp_data, dict)
|
||||
self.assertIn('switch_chassis_id', lldp_data)
|
||||
self.assertIn('switch_port_id', lldp_data)
|
||||
self.assertIn('switch_system_name', lldp_data)
|
||||
self.assertIn('switch_system_description', lldp_data)
|
||||
|
||||
# Verify expected values
|
||||
self.assertEqual('c4:7e:e0:e4:55:3f', lldp_data['switch_chassis_id'])
|
||||
self.assertEqual('Ethernet1/8', lldp_data['switch_port_id'])
|
||||
self.assertEqual('switch-01.example.com',
|
||||
lldp_data['switch_system_name'])
|
||||
self.assertEqual('Cisco IOS XE',
|
||||
lldp_data['switch_system_description'])
|
||||
|
||||
@mock.patch.object(redfish_utils, 'get_enabled_macs', autospec=True)
|
||||
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
|
||||
def test_collect_lldp_data_empty_lldp_receive(
|
||||
self, mock_get_system, mock_get_enabled_macs):
|
||||
"""Test LLDP collection with empty LLDPReceive (Dell scenario)"""
|
||||
system_mock = self.init_system_mock(mock_get_system.return_value)
|
||||
|
||||
mock_chassis = mock.Mock()
|
||||
mock_chassis.identity = 'System.Embedded.1'
|
||||
|
||||
# Mock Port with None lldp_receive
|
||||
mock_ethernet = mock.Mock()
|
||||
mock_ethernet.lldp_receive = None
|
||||
|
||||
mock_port = mock.Mock()
|
||||
mock_port.identity = 'NIC.Slot.1-1'
|
||||
mock_port.ethernet = mock_ethernet
|
||||
|
||||
mock_adapter = mock.Mock()
|
||||
mock_adapter.ports.get_members.return_value = [mock_port]
|
||||
|
||||
mock_chassis.network_adapters.get_members.return_value = [mock_adapter]
|
||||
system_mock.chassis = [mock_chassis]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
task.driver.inspect.inspect_hardware(task)
|
||||
|
||||
inventory = inspect_utils.get_inspection_data(self.node, self.context)
|
||||
|
||||
# parsed_lldp should not be in plugin_data when empty
|
||||
self.assertNotIn('parsed_lldp', inventory['plugin_data'])
|
||||
|
||||
@mock.patch.object(redfish_utils, 'get_enabled_macs', autospec=True)
|
||||
@mock.patch.object(redfish_utils, 'get_system', autospec=True)
|
||||
def test_collect_lldp_data_no_network_adapters(
|
||||
self, mock_get_system, mock_get_enabled_macs):
|
||||
"""Test LLDP collection when NetworkAdapters not available"""
|
||||
system_mock = self.init_system_mock(mock_get_system.return_value)
|
||||
|
||||
mock_chassis = mock.Mock()
|
||||
mock_chassis.identity = 'System.Embedded.1'
|
||||
# Raise exception when accessing network_adapters
|
||||
mock_chassis.network_adapters.get_members.side_effect = (
|
||||
Exception('Not found'))
|
||||
system_mock.chassis = [mock_chassis]
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
task.driver.inspect.inspect_hardware(task)
|
||||
|
||||
inventory = inspect_utils.get_inspection_data(self.node, self.context)
|
||||
|
||||
# Should handle gracefully and not include parsed_lldp
|
||||
self.assertNotIn('parsed_lldp', inventory['plugin_data'])
|
||||
|
||||
def test_convert_lldp_receive_to_dict_complete(self):
|
||||
"""Test dict conversion with complete LLDP data"""
|
||||
mock_lldp = mock.Mock()
|
||||
mock_lldp.chassis_id = 'c4:7e:e0:e4:55:3f'
|
||||
mock_lldp.chassis_id_subtype = mock.Mock(value='MacAddr')
|
||||
mock_lldp.port_id = 'Ethernet1/8'
|
||||
mock_lldp.port_id_subtype = mock.Mock(value='IfName')
|
||||
mock_lldp.system_name = 'switch-01'
|
||||
mock_lldp.system_description = 'Cisco IOS'
|
||||
mock_lldp.system_capabilities = [mock.Mock(value='Bridge')]
|
||||
mock_lldp.management_address_ipv4 = '192.168.1.1'
|
||||
mock_lldp.management_address_ipv6 = None
|
||||
mock_lldp.management_address_mac = None
|
||||
mock_lldp.management_vlan_id = 100
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
lldp_dict = task.driver.inspect._convert_lldp_receive_to_dict(
|
||||
mock_lldp)
|
||||
|
||||
# Verify dict format
|
||||
self.assertIsInstance(lldp_dict, dict)
|
||||
self.assertIn('switch_chassis_id', lldp_dict)
|
||||
self.assertIn('switch_port_id', lldp_dict)
|
||||
self.assertIn('switch_system_name', lldp_dict)
|
||||
self.assertIn('switch_system_description', lldp_dict)
|
||||
self.assertIn('switch_vlan_id', lldp_dict)
|
||||
|
||||
# Verify expected values
|
||||
self.assertEqual('c4:7e:e0:e4:55:3f', lldp_dict['switch_chassis_id'])
|
||||
self.assertEqual('Ethernet1/8', lldp_dict['switch_port_id'])
|
||||
self.assertEqual('switch-01', lldp_dict['switch_system_name'])
|
||||
self.assertEqual('Cisco IOS', lldp_dict['switch_system_description'])
|
||||
self.assertEqual(100, lldp_dict['switch_vlan_id'])
|
||||
|
||||
def test_convert_lldp_receive_to_dict_minimal(self):
|
||||
"""Test dict conversion with minimal LLDP data"""
|
||||
mock_lldp = mock.Mock()
|
||||
mock_lldp.chassis_id = 'aa:bb:cc:dd:ee:ff'
|
||||
mock_lldp.chassis_id_subtype = None
|
||||
mock_lldp.port_id = 'port-1'
|
||||
mock_lldp.port_id_subtype = None
|
||||
mock_lldp.system_name = None
|
||||
mock_lldp.system_description = None
|
||||
mock_lldp.system_capabilities = None
|
||||
mock_lldp.management_address_ipv4 = None
|
||||
mock_lldp.management_address_ipv6 = None
|
||||
mock_lldp.management_address_mac = None
|
||||
mock_lldp.management_vlan_id = None
|
||||
|
||||
with task_manager.acquire(self.context, self.node.uuid,
|
||||
shared=True) as task:
|
||||
lldp_dict = task.driver.inspect._convert_lldp_receive_to_dict(
|
||||
mock_lldp)
|
||||
|
||||
# Should have Chassis ID and Port ID only
|
||||
self.assertEqual(2, len(lldp_dict))
|
||||
self.assertIn('switch_chassis_id', lldp_dict)
|
||||
self.assertIn('switch_port_id', lldp_dict)
|
||||
self.assertEqual('aa:bb:cc:dd:ee:ff', lldp_dict['switch_chassis_id'])
|
||||
self.assertEqual('port-1', lldp_dict['switch_port_id'])
|
||||
|
||||
|
||||
|
||||
class ContinueInspectionTestCase(db_base.DbTestCase):
|
||||
def setUp(self):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
features:
|
||||
- |
|
||||
Adds Redfish LLDP data collection support to the Redfish inspection interface.
|
||||
This enables collecting Link Layer Discovery Protocol (LLDP) data from
|
||||
Redfish-compliant hardware during the inspection process. The collected
|
||||
data includes chassis ID, port ID, system name, system description,
|
||||
system capabilities, and management addresses, formatted as TLVs
|
||||
compatible with Ironic Python Agent inspection hooks and rules.
|
||||
Loading…
Add table
Reference in a new issue