Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/kerf/dtc/extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ def _create_comprehensive_fdt(self, tree: GlobalDeviceTree) -> bytes:
self._add_cpu_properties_sw(fdt_sw, tree.hardware.cpus)
self._add_memory_properties_sw(fdt_sw, tree.hardware.memory)

if tree.hardware.pci_host_bridges:
self._add_pci_host_bridges_sw(fdt_sw, tree.hardware.pci_host_bridges)

if tree.hardware.devices:
self._add_devices_section_sw(fdt_sw, tree.hardware.devices)

Expand Down Expand Up @@ -131,6 +134,21 @@ def _add_memory_properties_sw(self, fdt_sw, memory):
fdt_sw.property_u64("memory-base", memory.memory_pool_base)
fdt_sw.property_u64("memory-bytes", memory.memory_pool_bytes)

def _add_pci_host_bridges_sw(self, fdt_sw, bridges):
"""Add architecture-neutral PCI host bridge discovery metadata."""
import struct

fdt_sw.begin_node("pci-host-bridges")
for bridge in bridges:
fdt_sw.begin_node(f"host@{bridge.segment:04x},{bridge.bus_start:02x}")
fdt_sw.property_u32("segment", bridge.segment)
fdt_sw.property(
"bus-range", struct.pack(">II", bridge.bus_start, bridge.bus_end)
)
fdt_sw.property_u64("ecam-base", bridge.ecam_base)
fdt_sw.end_node()
fdt_sw.end_node()

def _add_devices_section_sw(self, fdt_sw, devices):
"""Add devices section using FdtSw."""
fdt_sw.begin_node("devices")
Expand Down
100 changes: 98 additions & 2 deletions src/kerf/dtc/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
MemoryAllocation,
NUMANode,
OverlayInstanceData,
PCIHostBridge,
TopologySection,
)

Expand Down Expand Up @@ -192,14 +193,48 @@ def _parse_hardware_inventory(self) -> HardwareInventory:

# Parse devices
devices = self._parse_devices(resources_node)
pci_host_bridges = self._parse_pci_host_bridges(resources_node)

return HardwareInventory(
cpus=cpus,
memory=memory,
topology=topology,
devices=devices
devices=devices,
pci_host_bridges=pci_host_bridges
)

def _parse_pci_host_bridges(self, resources_node: int) -> List[PCIHostBridge]:
"""Parse and validate PCI host bridge discovery metadata."""
try:
bridges_node = self.fdt.subnode_offset(resources_node, 'pci-host-bridges')
except libfdt.FdtException:
return []

bridges = []
try:
offset = self.fdt.first_subnode(bridges_node)
except libfdt.FdtException:
return bridges

while offset >= 0:
name = self.fdt.get_name(offset)
try:
segment = self.fdt.getprop(offset, 'segment').as_uint32()
bus_range = self.fdt.getprop(offset, 'bus-range').as_uint32_list()
ecam_base = self.fdt.getprop(offset, 'ecam-base').as_uint64()
except libfdt.FdtException as exc:
raise ParseError(f"Invalid PCI host bridge '{name}': {exc}") from exc
if len(bus_range) != 2:
raise ParseError(f"Invalid bus-range for PCI host bridge '{name}'")
bridge = PCIHostBridge(segment, bus_range[0], bus_range[1], ecam_base)
self._validate_pci_host_bridge(bridge, bridges, name)
bridges.append(bridge)
try:
offset = self.fdt.next_subnode(offset)
except libfdt.FdtException:
break
return bridges

def _parse_cpu_allocation(self, resources_node: int) -> CPUAllocation:
"""Parse CPU allocation from resources node."""
try:
Expand Down Expand Up @@ -713,14 +748,75 @@ def _parse_hardware_from_dts(self, dts_content: str) -> HardwareInventory:

# Parse devices
devices = self._parse_devices_from_dts(dts_content)
pci_host_bridges = self._parse_pci_host_bridges_from_dts(dts_content)

return HardwareInventory(
cpus=cpus,
memory=memory,
topology=topology,
devices=devices
devices=devices,
pci_host_bridges=pci_host_bridges
)

def _parse_pci_host_bridges_from_dts(self, dts_content: str) -> List[PCIHostBridge]:
"""Parse PCI host bridge metadata from DTS source."""
resources_text = self._extract_resources_section(dts_content)
if not resources_text:
return []
section_match = re.search(r'pci-host-bridges\s*\{', resources_text)
if not section_match:
return []
start = section_match.end() - 1
depth = 0
end = start
for index, char in enumerate(resources_text[start:], start):
if char == '{':
depth += 1
elif char == '}':
depth -= 1
if depth == 0:
end = index
break
if depth != 0:
raise ParseError("Unterminated pci-host-bridges section")
section = resources_text[start + 1:end]
bridges = []
for match in re.finditer(r'([\w@,.-]+)\s*\{([^{}]*)\}', section, re.DOTALL):
name, body = match.groups()
segment_match = re.search(r'segment\s*=\s*<([^>]+)>', body)
bus_match = re.search(r'bus-range\s*=\s*<([^>]+)>', body)
ecam_match = re.search(r'ecam-base\s*=.*?<([^>]+)>', body)
if not segment_match or not bus_match or not ecam_match:
raise ParseError(f"Invalid PCI host bridge '{name}'")
bus_cells = bus_match.group(1).split()
if len(bus_cells) != 2:
raise ParseError(f"Invalid bus-range for PCI host bridge '{name}'")
bridge = PCIHostBridge(
self._parse_hex_value(segment_match.group(1)),
int(bus_cells[0], 0),
int(bus_cells[1], 0),
self._parse_hex_value(ecam_match.group(1)),
)
self._validate_pci_host_bridge(bridge, bridges, name)
bridges.append(bridge)
return bridges

@staticmethod
def _validate_pci_host_bridge(bridge, existing, name):
if not 0 <= bridge.segment <= 0xffff:
raise ParseError(f"Invalid segment for PCI host bridge '{name}'")
if not 0 <= bridge.bus_start <= bridge.bus_end <= 0xff:
raise ParseError(f"Invalid bus-range for PCI host bridge '{name}'")
if not bridge.ecam_base or bridge.ecam_base % (1024 * 1024):
raise ParseError(f"Invalid ECAM base for PCI host bridge '{name}'")
for other in existing:
if (other.segment == bridge.segment and
bridge.bus_start <= other.bus_end and
bridge.bus_end >= other.bus_start):
raise ParseError(
f"Overlapping PCI host bridge bus ranges in segment {bridge.segment:04x}"
)

def _extract_resources_section(self, dts_content: str) -> Optional[str]:
"""Extract the resources section content with proper brace matching."""

Expand Down
10 changes: 9 additions & 1 deletion src/kerf/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
Data models for multikernel device tree representation.
"""

from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Set, Tuple
from enum import Enum

Expand Down Expand Up @@ -133,6 +133,13 @@ class DeviceInfo:
namespaces: Optional[int] = None
host_reserved_ns: Optional[int] = None
available_ns: Optional[List[int]] = None
@dataclass(frozen=True)
class PCIHostBridge:
"""Architecture-neutral PCI host bridge discovery metadata."""
segment: int
bus_start: int
bus_end: int
ecam_base: int


@dataclass
Expand Down Expand Up @@ -200,6 +207,7 @@ class HardwareInventory:
memory: MemoryAllocation
topology: Optional[TopologySection] = None
devices: Dict[str, DeviceInfo] = None
pci_host_bridges: List[PCIHostBridge] = field(default_factory=list)


@dataclass
Expand Down
22 changes: 22 additions & 0 deletions tests/test_baseline.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,28 @@ def test_read_baseline_not_found(self):
with pytest.raises(KernelInterfaceError, match="not found"):
manager.read_baseline()

def test_write_and_read_pci_host_bridge_metadata(self, sample_hardware):
"""PCI host bridge records survive the baseline model round trip."""
from kerf.models import GlobalDeviceTree, PCIHostBridge

sample_hardware.pci_host_bridges = [
PCIHostBridge(segment=0, bus_start=0, bus_end=255, ecam_base=0xb0000000)
]
with tempfile.NamedTemporaryFile(delete=False) as f:
baseline_path = f.name
try:
tree = GlobalDeviceTree(
hardware=sample_hardware, instances={}, device_references={}
)
manager = BaselineManager(baseline_path=baseline_path)
manager.write_baseline(tree)
assert manager.read_baseline().hardware.pci_host_bridges == (
sample_hardware.pci_host_bridges
)
finally:
if os.path.exists(baseline_path):
os.unlink(baseline_path)

def test_write_baseline_invalid_tree(self, sample_tree):
"""Test writing baseline with invalid tree (has instances)."""
with tempfile.NamedTemporaryFile(delete=False) as f:
Expand Down