diff --git a/src/kerf/dtc/extractor.py b/src/kerf/dtc/extractor.py index b119973..ddbbc57 100644 --- a/src/kerf/dtc/extractor.py +++ b/src/kerf/dtc/extractor.py @@ -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) @@ -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") diff --git a/src/kerf/dtc/parser.py b/src/kerf/dtc/parser.py index 9f604df..22c19fd 100644 --- a/src/kerf/dtc/parser.py +++ b/src/kerf/dtc/parser.py @@ -32,6 +32,7 @@ MemoryAllocation, NUMANode, OverlayInstanceData, + PCIHostBridge, TopologySection, ) @@ -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: @@ -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.""" diff --git a/src/kerf/models.py b/src/kerf/models.py index 35667a3..383ada8 100644 --- a/src/kerf/models.py +++ b/src/kerf/models.py @@ -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 @@ -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 @@ -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 diff --git a/tests/test_baseline.py b/tests/test_baseline.py index 1a1a027..afc2188 100644 --- a/tests/test_baseline.py +++ b/tests/test_baseline.py @@ -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: