Add comprehensive PCIe device support to Sushy

Implement complete Redfish PCIeDevice v1.19.0 specification support
including PCIeDevice and PCIeDeviceCollection resources embedded data support.

- Add PCIeDevice resource with schema compliance
- Support embedded PCIeDevices (Dell iDRAC pattern)
- Add methods for device inspection
- Comprehensive test coverage included onnboard devices too
- Handle both standard and embedded PCIeDevices patterns

Change-Id: I0326d5c2985e1b09828daa0f003b147f09628d3a
Signed-off-by: Nidhi Rai <nidhi.rai94@gmail.com>
This commit is contained in:
Nidhi Rai
2025-09-22 19:26:36 +05:30
parent faaf6cb687
commit 8388b3d9a6
7 changed files with 532 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
---
features:
- |
Adds comprehensive PCIeDevice resource support implementing the complete
Redfish PCIeDevice v1.19.0 specification. The new pcie_devices property
on System objects provides access to PCIe device collections with support
for both standard Redfish collections and embedded PCIeDevices (Dell iDRAC).
Features include full schema compliance, PCIe interface information and
slot details.

View File

@@ -0,0 +1,189 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# 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/PCIeDevice.v1_19_0.json
# Per DMTF DSP0268_2025.2 Section 6.96 PCIeDevice 1.19.0
import logging
from sushy.resources import base
from sushy.resources import common
from sushy import utils
LOG = logging.getLogger(__name__)
class PCIeInterfaceField(base.CompositeField):
"""PCIe interface information for the device."""
lanes_in_use = base.Field('LanesInUse', adapter=utils.int_or_none)
"""The number of PCIe lanes in use by this device."""
max_lanes = base.Field('MaxLanes', adapter=utils.int_or_none)
"""The number of PCIe lanes supported by this device."""
max_pcie_type = base.Field('MaxPCIeType')
"""The highest version of the PCIe specification supported by this
device."""
pcie_type = base.Field('PCIeType')
"""The version of the PCIe specification in use by this device."""
class SlotField(base.CompositeField):
"""Slot information for the PCIe device."""
lanes = base.Field('Lanes', adapter=utils.int_or_none)
"""The number of PCIe lanes supported by this slot."""
location = base.Field('Location')
"""The location of the PCIe slot."""
pcie_type = base.Field('PCIeType')
"""The PCIe specification this slot supports."""
slot_type = base.Field('SlotType')
"""The PCIe slot type."""
hot_pluggable = base.Field('HotPluggable', adapter=bool)
"""An indication of whether this PCIe slot supports hotplug."""
lane_splitting = base.Field('LaneSplitting')
"""The lane splitting strategy used in the PCIe slot."""
class PCIeDevice(base.ResourceBase):
"""Represents a PCIe device associated with a system."""
identity = base.Field('Id', required=True)
"""The PCIe device identity string"""
name = base.Field('Name')
"""The PCIe device name"""
description = base.Field('Description')
"""The PCIe device description"""
manufacturer = base.Field('Manufacturer')
"""The manufacturer of this PCIe device."""
model = base.Field('Model')
"""The model number for the PCIe device."""
serial_number = base.Field('SerialNumber')
"""The serial number for this PCIe device."""
part_number = base.Field('PartNumber')
"""The part number for this PCIe device."""
sku = base.Field('SKU')
"""The stock-keeping unit for this PCIe device."""
device_type = base.Field('DeviceType')
"""The device type for this PCIe device."""
firmware_version = base.Field('FirmwareVersion')
"""The version of firmware for this PCIe device."""
asset_tag = base.Field('AssetTag')
"""The user-assigned asset tag for this PCIe device."""
status = common.StatusField('Status')
"""The status and health of the resource and its subordinate resources."""
pcie_interface = PCIeInterfaceField('PCIeInterface')
"""The PCIe interface details for this device."""
slot = SlotField('Slot')
"""Information about the slot for this PCIe device."""
links = base.Field('Links')
"""Links to related resources."""
class PCIeDeviceCollection(base.ResourceCollectionBase):
@property
def _resource_type(self):
return PCIeDevice
@property
@utils.cache_it
def device_count(self):
"""The number of PCIe devices in the collection.
Returns the cached value until it (or its parent resource)
is refreshed.
"""
return len(self.get_members())
def __init__(self, connector, path, redfish_version=None, registries=None,
root=None, embedded_data=None):
if path == "/empty":
self._conn = connector
self._path = path
self._json = {'Members': []}
self.redfish_version = redfish_version
self._registries = registries
self._root = root
self._is_stale = False
return
if embedded_data is not None:
self._conn = connector
self._path = path
self._json = {'Members': embedded_data}
self.redfish_version = redfish_version
self._registries = registries
self._root = root
self._is_stale = False
return
super().__init__(connector, path, redfish_version, registries, root)
@property
def members_identities(self):
if self._path == "/empty":
return []
if hasattr(self, '_json') and self._json and 'Members' in self._json:
return tuple(m['@odata.id'] for m in self._json['Members']
if isinstance(m, dict) and '@odata.id' in m)
return super().members_identities
def get_members(self):
if self._path == "/empty":
return []
# Handle embedded PCIeDevices case
if hasattr(self, '_json') and self._json and 'Members' in self._json:
device_objects = []
for member in self._json['Members']:
if isinstance(member, dict) and '@odata.id' in member:
device_path = member['@odata.id']
try:
# Fetch device data and create object
device_response = self._conn.get(device_path)
if device_response:
device_obj = PCIeDevice(
self._conn, device_path,
redfish_version=self.redfish_version,
registries=self._registries, root=self._root
)
device_obj._json = device_response.json()
device_objects.append(device_obj)
except Exception as e:
LOG.warning(
'Error fetching PCIe device at path %s: %s',
device_path, str(e))
continue # Skip failed devices but continue processing
return device_objects
# Standard collection behavior
return super().get_members()

View File

@@ -32,6 +32,7 @@ from sushy.resources import settings
from sushy.resources.system import bios
from sushy.resources.system import constants as sys_cons
from sushy.resources.system import ethernet_interface
from sushy.resources.system import pcie_device
from sushy.resources.system import processor
from sushy.resources.system import secure_boot
from sushy.resources.system import simple_storage as sys_simple_storage
@@ -674,6 +675,32 @@ class System(base.ResourceBase):
redfish_version=self.redfish_version, registries=self.registries,
root=self.root)
@property
@utils.cache_it
def pcie_devices(self):
"""Property to reference PCIeDeviceCollection instance"""
try:
pcie_path = utils.get_sub_resource_path_by(self, "PCIeDevices")
return pcie_device.PCIeDeviceCollection(
self._conn, pcie_path,
redfish_version=self.redfish_version,
registries=self.registries, root=self.root)
except exceptions.MissingAttributeError:
# Check if PCIeDevices is embedded in System JSON
if (hasattr(self, 'json') and self.json
and 'PCIeDevices' in self.json):
pcie_devices_data = self.json['PCIeDevices']
if isinstance(pcie_devices_data, list) and pcie_devices_data:
return pcie_device.PCIeDeviceCollection(
self._conn, "/embedded",
redfish_version=self.redfish_version,
registries=self.registries, root=self.root,
embedded_data=pcie_devices_data)
return pcie_device.PCIeDeviceCollection(
self._conn, "/empty",
redfish_version=self.redfish_version,
registries=self.registries, root=self.root)
class SystemCollection(base.ResourceCollectionBase):

View File

@@ -0,0 +1,71 @@
{
"@odata.context": "/redfish/v1/$metadata#PCIeDevice.PCIeDevice",
"@odata.etag": "\"1756839817\"",
"@odata.id": "/redfish/v1/Chassis/System.Embedded.1/PCIeDevices/196-0",
"@odata.type": "#PCIeDevice.v1_11_1.PCIeDevice",
"AssetTag": null,
"Description": "BCM957414A4142CC 10Gb/25Gb Ethernet PCIe",
"DeviceType": "MultiFunction",
"FirmwareVersion": "224.1.102.0",
"Id": "196-0",
"Links": {
"Chassis": [
{
"@odata.id": "/redfish/v1/Chassis/System.Embedded.1"
}
],
"Chassis@odata.count": 1,
"Oem": {
"Dell": {
"@odata.type": "#DellOem.v1_3_0.DellOemLinks",
"CPUAffinity": [
{
"@odata.id": "/redfish/v1/Systems/System.Embedded.1/Processors/CPU.Socket.1"
}
],
"CPUAffinity@odata.count": 1
}
},
"PCIeFunctions": [
{
"@odata.id": "/redfish/v1/Chassis/System.Embedded.1/PCIeDevices/196-0/PCIeFunctions/196-0-1"
},
{
"@odata.id": "/redfish/v1/Chassis/System.Embedded.1/PCIeDevices/196-0/PCIeFunctions/196-0-0"
}
],
"PCIeFunctions@odata.count": 2,
"PCIeFunctions@Redfish.Deprecated": "Please migrate to PCIeFunctions property in the root resource."
},
"Manufacturer": "Broadcom Inc. and subsidiaries",
"Model": null,
"Name": "BCM957414A4142CC 10Gb/25Gb Ethernet PCIe",
"PartNumber": null,
"SKU": null,
"SerialNumber": null,
"Slot": {
"Lanes": 8,
"Location": {
"PartLocation": {
"LocationOrdinalValue": 1,
"LocationType": "Slot"
}
},
"PCIeType": "Gen5",
"SlotType": "HalfLength"
},
"PCIeInterface": {
"MaxPCIeType": "Gen5",
"PCIeType": "Gen5",
"MaxLanes": 8,
"LanesInUse": 8
},
"Status": {
"State": "Enabled",
"Health": "OK",
"HealthRollup": "OK"
},
"PCIeFunctions": {
"@odata.id": "/redfish/v1/Chassis/System.Embedded.1/PCIeDevices/196-0/PCIeFunctions"
}
}

View File

@@ -0,0 +1,19 @@
{
"@odata.context": "/redfish/v1/$metadata#PCIeDeviceCollection.PCIeDeviceCollection",
"@odata.etag": "\"1756839817\"",
"@odata.id": "/redfish/v1/Chassis/System.Embedded.1/PCIeDevices",
"@odata.type": "#PCIeDeviceCollection.PCIeDeviceCollection",
"Description": "Collection of PCIe Devices",
"Members": [
{
"@odata.id": "/redfish/v1/Chassis/System.Embedded.1/PCIeDevices/196-0",
"Id": "196-0"
},
{
"@odata.id": "/redfish/v1/Chassis/System.Embedded.1/PCIeDevices/65-0",
"Id": "65-0"
}
],
"Members@odata.count": 2,
"Name": "PCIe Device Collection"
}

View File

@@ -0,0 +1,48 @@
{
"@odata.context": "/redfish/v1/$metadata#PCIeDevice.PCIeDevice",
"@odata.etag": "\"1756839815\"",
"@odata.id": "/redfish/v1/Chassis/System.Embedded.1/PCIeDevices/128-1",
"@odata.type": "#PCIeDevice.v1_18_0.PCIeDevice",
"AssetTag": null,
"Description": "Advanced Micro Devices, Inc. [AMD]",
"DeviceType": "MultiFunction",
"FirmwareVersion": "",
"Id": "128-1",
"Links": {
"Chassis": [
{
"@odata.id": "/redfish/v1/Chassis/System.Embedded.1"
}
],
"Chassis@odata.count": 1,
"Oem": {
"Dell": {
"@odata.type": "#DellOem.v1_3_0.DellOemLinks",
"CPUAffinity": [],
"CPUAffinity@odata.count": 0
}
},
"PCIeFunctions": [
{
"@odata.id": "/redfish/v1/Chassis/System.Embedded.1/PCIeDevices/128-1/PCIeFunctions/128-1-0"
}
],
"PCIeFunctions@odata.count": 1,
"PCIeFunctions@Redfish.Deprecated": "Please migrate to PCIeFunctions property in the root resource."
},
"Manufacturer": "Advanced Micro Devices, Inc. [AMD]",
"Model": null,
"Name": "Advanced Micro Devices, Inc. [AMD]",
"PartNumber": null,
"SKU": null,
"SerialNumber": null,
"Slot": {},
"Status": {
"State": "Enabled",
"Health": "OK",
"HealthRollup": "OK"
},
"PCIeFunctions": {
"@odata.id": "/redfish/v1/Chassis/System.Embedded.1/PCIeDevices/128-1/PCIeFunctions"
}
}

View File

@@ -0,0 +1,168 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
from unittest import mock
from sushy.resources import constants as res_cons
from sushy.resources.system import pcie_device
from sushy.tests.unit import base
class PCIeDeviceTestCase(base.TestCase):
def setUp(self):
super().setUp()
self.conn = mock.Mock()
with open('sushy/tests/unit/json_samples/pcie_device.json') as f:
self.json_doc = json.load(f)
self.conn.get.return_value.json.return_value = self.json_doc
self.pcie_dev = pcie_device.PCIeDevice(
self.conn,
'/redfish/v1/Chassis/System.Embedded.1/PCIeDevices/196-0',
redfish_version='1.0.2')
def test__parse_attributes(self):
self.pcie_dev._parse_attributes(self.json_doc)
self.assertEqual('1.0.2', self.pcie_dev.redfish_version)
self.assertEqual('196-0', self.pcie_dev.identity)
self.assertEqual('BCM957414A4142CC 10Gb/25Gb Ethernet PCIe',
self.pcie_dev.name)
self.assertEqual('BCM957414A4142CC 10Gb/25Gb Ethernet PCIe',
self.pcie_dev.description)
self.assertEqual('Broadcom Inc. and subsidiaries',
self.pcie_dev.manufacturer)
self.assertEqual('MultiFunction', self.pcie_dev.device_type)
self.assertEqual('224.1.102.0', self.pcie_dev.firmware_version)
self.assertIsNone(self.pcie_dev.asset_tag)
self.assertIsNone(self.pcie_dev.serial_number)
self.assertIsNone(self.pcie_dev.model)
self.assertIsNone(self.pcie_dev.part_number)
self.assertIsNone(self.pcie_dev.sku)
def test_pcie_interface(self):
pcie_if = self.pcie_dev.pcie_interface
self.assertEqual('Gen5', pcie_if.pcie_type)
self.assertEqual('Gen5', pcie_if.max_pcie_type)
self.assertEqual(8, pcie_if.lanes_in_use)
self.assertEqual(8, pcie_if.max_lanes)
def test_slot_information(self):
slot = self.pcie_dev.slot
self.assertEqual('HalfLength', slot.slot_type)
self.assertEqual('Gen5', slot.pcie_type)
self.assertEqual(8, slot.lanes)
self.assertIsNotNone(slot.location)
self.assertIsNone(slot.lane_splitting)
self.assertIsNone(slot.hot_pluggable)
def test_status_field(self):
status = self.pcie_dev.status
self.assertEqual(res_cons.State.ENABLED, status.state)
self.assertEqual(res_cons.Health.OK, status.health)
self.assertEqual(res_cons.Health.OK, status.health_rollup)
def test_links_field(self):
links = self.pcie_dev.links
self.assertIsNotNone(links)
class PCIeDeviceOnboardTestCase(base.TestCase):
def setUp(self):
super().setUp()
self.conn = mock.Mock()
with open('sushy/tests/unit/json_samples/'
'pcie_device_onboard.json') as f:
self.json_doc = json.load(f)
self.conn.get.return_value.json.return_value = self.json_doc
self.onboard_dev = pcie_device.PCIeDevice(
self.conn,
'/redfish/v1/Chassis/System.Embedded.1/PCIeDevices/128-1',
redfish_version='1.0.2')
def test_onboard_device_attributes(self):
self.assertEqual('128-1', self.onboard_dev.identity)
self.assertEqual('Advanced Micro Devices, Inc. [AMD]',
self.onboard_dev.name)
self.assertEqual('Advanced Micro Devices, Inc. [AMD]',
self.onboard_dev.manufacturer)
self.assertEqual('MultiFunction', self.onboard_dev.device_type)
self.assertEqual('', self.onboard_dev.firmware_version)
def test_onboard_device_empty_slot(self):
slot = self.onboard_dev.slot
self.assertIsNotNone(slot)
# Empty slot object should have None values
self.assertIsNone(slot.slot_type)
self.assertIsNone(slot.lanes)
self.assertIsNone(slot.pcie_type)
class PCIeDeviceCollectionTestCase(base.TestCase):
def setUp(self):
super().setUp()
self.conn = mock.Mock()
with open('sushy/tests/unit/json_samples/'
'pcie_device_collection.json') as f:
self.json_doc = json.load(f)
self.conn.get.return_value.json.return_value = self.json_doc
self.collection = pcie_device.PCIeDeviceCollection(
self.conn, '/redfish/v1/Chassis/System.Embedded.1/PCIeDevices',
redfish_version='1.0.2')
def test__parse_attributes(self):
self.collection._parse_attributes(self.json_doc)
self.assertEqual('1.0.2', self.collection.redfish_version)
self.assertEqual('PCIe Device Collection', self.collection.name)
self.assertEqual(
('/redfish/v1/Chassis/System.Embedded.1/PCIeDevices/196-0',
'/redfish/v1/Chassis/System.Embedded.1/PCIeDevices/65-0'),
self.collection.members_identities)
@mock.patch.object(pcie_device, 'PCIeDevice', autospec=True)
def test_get_member(self, mock_pcie_device):
self.collection.get_member(
'/redfish/v1/Chassis/System.Embedded.1/'
'PCIeDevices/196-0')
mock_pcie_device.assert_called_once_with(
self.collection._conn,
'/redfish/v1/Chassis/System.Embedded.1/'
'PCIeDevices/196-0',
redfish_version=self.collection.redfish_version,
registries=None, root=self.collection.root)
@mock.patch.object(pcie_device, 'PCIeDevice', autospec=True)
def test_get_members(self, mock_pcie_device):
members = self.collection.get_members()
calls = [
mock.call(self.collection._conn,
'/redfish/v1/Chassis/System.Embedded.1/'
'PCIeDevices/196-0',
redfish_version=self.collection.redfish_version,
registries=None, root=self.collection.root),
mock.call(self.collection._conn,
'/redfish/v1/Chassis/System.Embedded.1/'
'PCIeDevices/65-0',
redfish_version=self.collection.redfish_version,
registries=None, root=self.collection.root)
]
mock_pcie_device.assert_has_calls(calls)
self.assertIsInstance(members, list)
self.assertEqual(2, len(members))