From cb6d5cc9d113441f3619bba7a141e3df1e81af6c Mon Sep 17 00:00:00 2001 From: Nidhi Rai Date: Mon, 10 Nov 2025 22:10:47 +0530 Subject: [PATCH] Add complete LLDP fields to Port.Ethernet.LLDPReceive per DMTF Redfish v1.12.0 Current sushy only implements 2 of 11 LLDP fields (chassis_id, port_id) from DMTF Redfish Port Schema v1.12.0. This enhancement adds the remaining fields: - chassis_id_subtype, port_id_subtype (IEEE 802.1AB subtypes) - system_name, system_description (switch info) - system_capabilities (Bridge/Router/etc.) - management_address_ipv4/ipv6/mac, management_vlan_id (mgmt info) References: - DMTF Redfish Port v1.12.0 - IEEE 802.1AB-2016 LLDP Change-Id: I2494eafc28337f9b5911308799e899bc79e7ca31 Signed-off-by: Nidhi Rai --- ...e-fields-enhancement-f1e3d4a5b2c6d789.yaml | 22 ++++++ sushy/resources/system/network/constants.py | 68 +++++++++++++++++ sushy/resources/system/port.py | 46 +++++++++++- sushy/tests/unit/json_samples/port.json | 13 +++- .../tests/unit/resources/system/test_port.py | 74 +++++++++++++++++++ 5 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/add-lldp-receive-fields-enhancement-f1e3d4a5b2c6d789.yaml diff --git a/releasenotes/notes/add-lldp-receive-fields-enhancement-f1e3d4a5b2c6d789.yaml b/releasenotes/notes/add-lldp-receive-fields-enhancement-f1e3d4a5b2c6d789.yaml new file mode 100644 index 00000000..4b842c5c --- /dev/null +++ b/releasenotes/notes/add-lldp-receive-fields-enhancement-f1e3d4a5b2c6d789.yaml @@ -0,0 +1,22 @@ +--- +features: + - | + Enhances the existing LLDPReceiveField class by adding new LLDP receive data + fields as specified in Redfish Port Schema v1.12.0. The existing class already + contained chassis_id and port_id fields, and this enhancement adds 9 additional + LLDP receive properties. + + New LLDP receive fields added: + - chassis_id_subtype: IEEE 802 chassis ID subtype identification + - port_id_subtype: IEEE 802 port ID subtype with MAC address handling + - system_name: System name received from remote link partner + - system_description: System description from remote link partner + - system_capabilities: Network device capabilities mapping + - management_address_ipv4: IPv4 management address + - management_address_ipv6: IPv6 management address + - management_address_mac: MAC management address + - management_vlan_id: Management VLAN ID configuration (0-4095) + + This enhancement provides LLDP receive information through + the EthernetField.lldp_receive property, enabling better network topology + discovery. \ No newline at end of file diff --git a/sushy/resources/system/network/constants.py b/sushy/resources/system/network/constants.py index c5c29e8b..aea2eb20 100644 --- a/sushy/resources/system/network/constants.py +++ b/sushy/resources/system/network/constants.py @@ -137,3 +137,71 @@ class PortLinkStatus(enum.Enum): TRAINING = 'Training' """This physical link on this interface is training.""" + + +class IEEE802IdSubtype(enum.Enum): + """IEEE 802.1AB ID Subtypes for Chassis ID and Port ID. + + Based on IEEE 802.1AB-2009 and DMTF Redfish Port Schema v1.12.0 + """ + + CHASSIS_COMP = 'ChassisComp' + """Chassis component, based on entPhysicalAlias in RFC4133.""" + + IF_ALIAS = 'IfAlias' + """Interface alias, based on the ifAlias MIB object.""" + + PORT_COMP = 'PortComp' + """Port component, based on entPhysicalAlias in RFC4133.""" + + MAC_ADDR = 'MacAddr' + """MAC address, based on agent-detected unicast source (IEEE 802).""" + + NETWORK_ADDR = 'NetworkAddr' + """Network address, based on agent-detected network address.""" + + IF_NAME = 'IfName' + """Interface name, based on the ifName MIB object.""" + + AGENT_ID = 'AgentId' + """Agent circuit ID, based on agent-local identifier (RFC3046).""" + + LOCAL_ASSIGN = 'LocalAssign' + """Locally assigned, based on alphanumeric value.""" + + NOT_TRANSMITTED = 'NotTransmitted' + """No data to be sent to/received from remote partner.""" + + +class LLDPSystemCapabilities(enum.Enum): + """LLDP System Capabilities. + + Based on IEEE 802.1AB and DMTF Redfish Port Schema v1.12.0 + """ + + NONE = 'None' + """System capabilities are transmitted, but no capabilities are set.""" + + BRIDGE = 'Bridge' + """'bridge' capability.""" + + DOCSIS_CABLE_DEVICE = 'DOCSISCableDevice' + """'DOCSIS cable device' capability.""" + + OTHER = 'Other' + """'other' capability.""" + + REPEATER = 'Repeater' + """'repeater' capability.""" + + ROUTER = 'Router' + """'router' capability.""" + + STATION = 'Station' + """'station' capability.""" + + TELEPHONE = 'Telephone' + """'telephone' capability.""" + + WLAN_ACCESS_POINT = 'WLANAccessPoint' + """'WLAN access point' capability.""" diff --git a/sushy/resources/system/port.py b/sushy/resources/system/port.py index 1f197aad..c01c6db6 100644 --- a/sushy/resources/system/port.py +++ b/sushy/resources/system/port.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. # This is referred from Redfish standard schema. -# https://redfish.dmtf.org/schemas/v1/Port.v1_8_0.json +# https://redfish.dmtf.org/schemas/v1/Port.v1_12_0.json from sushy.resources import base from sushy.resources import common @@ -18,12 +18,56 @@ from sushy.resources.system.network import constants class LLDPReceiveField(base.CompositeField): + """LLDP data being received on this link. + + Based on DMTF Redfish Port Schema v1.12.0 + https://redfish.dmtf.org/schemas/v1/Port.v1_12_0.json#/definitions/LLDPReceive + """ + chassis_id = base.Field("ChassisId") """chassis ID received from the remote partner across this link.""" + chassis_id_subtype = base.MappedField("ChassisIdSubtype", + constants.IEEE802IdSubtype) + """The type of identifier used for the chassis ID""" + port_id = base.Field("PortId") """A colon delimited string of hexadecimal octets identifying a port.""" + port_id_subtype = base.MappedField("PortIdSubtype", + constants.IEEE802IdSubtype) + """The port ID subtype received from the remote partner""" + + # TLV Type 3 - Time To Live not in current schema + + # TLV Type 4 - Port Description ,not in schema + + # TLV Type 5 - System Name + system_name = base.Field("SystemName") + """The system name received from the remote partner across this link.""" + + # TLV Type 6 - System Description + system_description = base.Field("SystemDescription") + """The system description received from the remote partner.""" + + # TLV Type 7 - System Capabilities + system_capabilities = base.MappedListField( + "SystemCapabilities", constants.LLDPSystemCapabilities) + """The system capabilities received from the remote partner.""" + + # TLV Type 8 - Management Addresses + management_address_ipv4 = base.Field("ManagementAddressIPv4") + """The IPv4 management address received from the remote partner.""" + + management_address_ipv6 = base.Field("ManagementAddressIPv6") + """The IPv6 management address received from the remote partner.""" + + management_address_mac = base.Field("ManagementAddressMAC") + """The management MAC address received from the remote partner.""" + + management_vlan_id = base.Field("ManagementVlanId", adapter=int) + """The management VLAN ID received from the remote partner (0-4095).""" + class EthernetField(base.CompositeField): associated_mac_addresses = base.Field( diff --git a/sushy/tests/unit/json_samples/port.json b/sushy/tests/unit/json_samples/port.json index 3dfa41c7..23d8c720 100644 --- a/sushy/tests/unit/json_samples/port.json +++ b/sushy/tests/unit/json_samples/port.json @@ -13,10 +13,17 @@ "FlowControlConfiguration": "None", "FlowControlStatus": "None", "LLDPReceive": { - "ChassisId": "Not Available", - "ChassisIdSubtype": null, + "ChassisId": "c4:7e:e0:e4:55:3f", + "ChassisIdSubtype": "MacAddr", "PortId": "0A:1B:2C:3D:4E:5F:6A:7B:8C:9D:0E:1F:2A", - "PortIdSubtype": null + "PortIdSubtype": "IfName", + "SystemName": "switch-00.example.com", + "SystemDescription": "Test Software, Version 00.00.00", + "SystemCapabilities": ["Bridge", "Router"], + "ManagementAddressIPv4": "192.168.1.1", + "ManagementAddressIPv6": "fe80::1", + "ManagementAddressMAC": "c4:7e:e0:e4:55:40", + "ManagementVlanId": 100 }, "WakeOnLANEnabled": false }, diff --git a/sushy/tests/unit/resources/system/test_port.py b/sushy/tests/unit/resources/system/test_port.py index b2548516..beecc7ab 100644 --- a/sushy/tests/unit/resources/system/test_port.py +++ b/sushy/tests/unit/resources/system/test_port.py @@ -53,6 +53,80 @@ class PortTestCase(base.TestCase): self.port.ethernet.flow_control_status) self.assertEqual(net_cons.PortLinkStatus.LINKUP, self.port.link_status) + def test_lldp_receive_all_fields(self): + """Test all enhanced LLDP fields are parsed correctly""" + self.port._parse_attributes(self.json_doc) + + lldp = self.port.ethernet.lldp_receive + + # Test TLV Type 1 - Chassis ID with subtype + self.assertEqual('c4:7e:e0:e4:55:3f', lldp.chassis_id) + self.assertEqual(net_cons.IEEE802IdSubtype.MAC_ADDR, + lldp.chassis_id_subtype) + + # Test TLV Type 2 - Port ID with subtype + self.assertEqual('0A:1B:2C:3D:4E:5F:6A:7B:8C:9D:0E:1F:2A', + lldp.port_id) + self.assertEqual(net_cons.IEEE802IdSubtype.IF_NAME, + lldp.port_id_subtype) + + # Test TLV Type 5 - System Name + self.assertEqual('switch-00.example.com', lldp.system_name) + + # Test TLV Type 6 - System Description + self.assertEqual('Test Software, Version 00.00.00', + lldp.system_description) + + # Test TLV Type 7 - System Capabilities + self.assertIsNotNone(lldp.system_capabilities) + self.assertEqual(2, len(lldp.system_capabilities)) + self.assertIn(net_cons.LLDPSystemCapabilities.BRIDGE, + lldp.system_capabilities) + self.assertIn(net_cons.LLDPSystemCapabilities.ROUTER, + lldp.system_capabilities) + + # Test TLV Type 8 - Management Addresses + self.assertEqual('192.168.1.1', lldp.management_address_ipv4) + self.assertEqual('fe80::1', lldp.management_address_ipv6) + self.assertEqual('c4:7e:e0:e4:55:40', lldp.management_address_mac) + self.assertEqual(100, lldp.management_vlan_id) + + def test_lldp_receive_minimal_data(self): + """Test LLDP with minimal data (only mandatory fields)""" + minimal_doc = self.json_doc.copy() + minimal_doc['Ethernet']['LLDPReceive'] = { + "ChassisId": "aa:bb:cc:dd:ee:ff", + "PortId": "port-1" + } + + self.port._parse_attributes(minimal_doc) + lldp = self.port.ethernet.lldp_receive + + # Mandatory fields present + self.assertEqual('aa:bb:cc:dd:ee:ff', lldp.chassis_id) + self.assertEqual('port-1', lldp.port_id) + + # Optional fields None + self.assertIsNone(lldp.chassis_id_subtype) + self.assertIsNone(lldp.port_id_subtype) + self.assertIsNone(lldp.system_name) + self.assertIsNone(lldp.system_description) + self.assertIsNone(lldp.system_capabilities) + self.assertIsNone(lldp.management_address_ipv4) + + def test_lldp_receive_empty(self): + """Test empty LLDPReceive (Dell scenario)""" + empty_doc = self.json_doc.copy() + empty_doc['Ethernet']['LLDPReceive'] = {} + + self.port._parse_attributes(empty_doc) + lldp = self.port.ethernet.lldp_receive + + # All fields should be None + self.assertIsNone(lldp.chassis_id) + self.assertIsNone(lldp.port_id) + self.assertIsNone(lldp.system_name) + class PortCollectionTestCase(base.TestCase):