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
17 changes: 13 additions & 4 deletions src/kerf/dtc/overlay.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,6 @@ def _create_overlay_dtb(

fdt_sw.property_u64("memory-bytes", instance.resources.memory_bytes)

if instance.resources.devices:
stringlist_data = b'\0'.join(d.encode('utf-8') for d in instance.resources.devices) + b'\0'
fdt_sw.property("device-names", stringlist_data)

if instance.resources.numa_nodes:
numa_data = struct.pack(
">" + "I" * len(instance.resources.numa_nodes), *instance.resources.numa_nodes
Expand Down Expand Up @@ -341,6 +337,19 @@ def _create_overlay_dtb(
fdt_sw.end_node() # End fragment
fragment_id += 1

if instance.resources.devices:
fdt_sw.begin_node(f"fragment@{fragment_id:x}")
fdt_sw.property_string(
"target-path", f"{self.INSTANCES_PATH}/{name}"
)
fdt_sw.begin_node("__overlay__")
self._device_op(
fdt_sw, "device-add", sorted(instance.resources.devices)
)
fdt_sw.end_node() # End __overlay__
fdt_sw.end_node() # End fragment
fragment_id += 1

for name in instances_to_remove:
fdt_sw.begin_node(f"fragment@{fragment_id:x}")
fdt_sw.property_string("target-path", self.INSTANCES_PATH)
Expand Down
213 changes: 151 additions & 62 deletions src/kerf/dtc/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,11 @@

import re
import struct
from typing import Dict, List, Optional, Tuple
from typing import Dict, List, Optional, TYPE_CHECKING, Tuple

import libfdt

from ..exceptions import ParseError
from ..pool_diff import ANY_NODE
from .cells import unpack_cpu_ids
from ..models import (
CPUAllocation,
DeviceInfo,
Expand All @@ -38,6 +36,11 @@
PoolMemoryRegion,
TopologySection,
)
from ..pool_diff import ANY_NODE
from .cells import unpack_cpu_ids

if TYPE_CHECKING:
from ..models import CPUTopology


_LEGACY_MEMORY_ERROR = (
Expand Down Expand Up @@ -106,6 +109,21 @@ def parse_dtb_from_bytes(self, dtb_data: bytes) -> GlobalDeviceTree:
except Exception as e:
raise ParseError(f"Failed to parse DTB from bytes: {e}") from e

def parse_devices_from_bytes(self, dtb_data: bytes) -> Dict[str, DeviceInfo]:
"""Parse only a DTB's device inventory.

Instance trees describe their assigned memory differently from a live
pool, but use the same hierarchical PCI representation. Callers that
only need to verify device membership can therefore avoid parsing the
unrelated pool metadata.
"""
try:
self.fdt = libfdt.Fdt(dtb_data)
resources = self.fdt.path_offset('/resources')
return self._parse_devices(resources)
except libfdt.FdtException as e:
raise ParseError(f"Failed to parse devices from DTB: {e}") from e

def _build_global_tree(self) -> GlobalDeviceTree:
"""Build GlobalDeviceTree from parsed FDT."""
try:
Expand Down Expand Up @@ -312,32 +330,90 @@ def _parse_memory_allocation(self, resources_node: int) -> MemoryAllocation:
)

def _parse_devices(self, resources_node: int) -> Dict[str, DeviceInfo]:
"""Parse device information from resources node."""
"""Parse flat platform devices and the kernel's PCI hierarchy."""
devices = {}

try:
devices_node = self.fdt.subnode_offset(resources_node, 'devices')
except libfdt.FdtException:
return devices
devices_node = -1

# Iterate through device nodes
offset = self.fdt.first_subnode(devices_node)
while offset >= 0:
name = self.fdt.get_name(offset)
try:
device_info = self._parse_device_info(offset, name)
devices[name] = device_info
except ParseError:
# Skip nodes that don't have required properties (not valid devices)
pass
if devices_node >= 0:
# Platform devices and legacy PCI inventories are flat here.
offset = self.fdt.first_subnode(devices_node)
while offset >= 0:
name = self.fdt.get_name(offset)
try:
device_info = self._parse_device_info(offset, name)
devices[name] = device_info
except ParseError:
# Skip nodes that don't have required properties.
pass
try:
offset = self.fdt.next_subnode(offset)
except libfdt.FdtException:
break

known_pci_ids = {
device.pci_id for device in devices.values() if device.pci_id
}
root = self.fdt.path_offset('/')
for node in self._subnodes(root):
compatible = self._optional_prop(node, 'compatible')
if compatible is None:
continue
try:
offset = self.fdt.next_subnode(offset)
except libfdt.FdtException:
# No more subnodes
break
is_pci_root = compatible.as_str() == 'multikernel,pci-host-bridge'
except (AttributeError, ValueError):
is_pci_root = False
if not is_pci_root:
continue
domain = self._optional_u32(node, 'linux,pci-domain', 0)
self._parse_pci_bus(node, domain, devices, known_pci_ids)

return devices

def _parse_pci_bus(
self,
bus_node: int,
domain: int,
devices: Dict[str, DeviceInfo],
known_pci_ids: set[str],
) -> None:
"""Recover canonical BDFs from hierarchical PCI ``reg`` cells."""
for node in self._subnodes(bus_node):
reg = self._optional_prop(node, 'reg')
if reg is None or len(reg) < 4:
continue

phys_hi = struct.unpack('>I', bytes(reg)[:4])[0]
vendor = self._optional_prop(node, 'vendor-id')
if vendor is None:
self._parse_pci_bus(node, domain, devices, known_pci_ids)
continue

device = self._optional_prop(node, 'device-id')
if device is None:
continue

bus = (phys_hi >> 16) & 0xff
devfn = (phys_hi >> 8) & 0xff
slot = (devfn >> 3) & 0x1f
function = devfn & 0x7
pci_id = f'{domain:04x}:{bus:02x}:{slot:02x}.{function:x}'
if pci_id in known_pci_ids:
continue

devices[pci_id] = DeviceInfo(
name=pci_id,
compatible='pci',
device_type='pci',
pci_id=pci_id,
vendor_id=vendor.as_uint32(),
device_id=device.as_uint32(),
)
known_pci_ids.add(pci_id)

def _parse_device_info(self, node_offset: int, name: str) -> DeviceInfo:
"""Parse individual device information."""
compatible = ""
Expand Down Expand Up @@ -783,14 +859,14 @@ def _parse_hardware_from_dts(self, dts_content: str) -> HardwareInventory:
devices=devices
)

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

resources_start = re.search(r'resources\s*\{', dts_content)
if not resources_start:
section_start = re.search(rf'{re.escape(section_name)}\s*\{{', dts_content)
if not section_start:
return None

start_pos = resources_start.end() - 1
start_pos = section_start.end() - 1
brace_count = 0
end_pos = start_pos

Expand All @@ -804,27 +880,47 @@ def _extract_resources_section(self, dts_content: str) -> Optional[str]:
break

if brace_count == 0:
return dts_content[start_pos+1:end_pos]
return dts_content[start_pos + 1:end_pos]
return None

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

@staticmethod
def _parse_cell_values(values: str) -> List[int]:
"""Parse integer cells after removing DTS comments."""

values = re.sub(r'/\*.*?\*/', ' ', values, flags=re.DOTALL)
values = re.sub(r'//[^\n]*', ' ', values)
return [int(value, 0) for value in values.split()]

def _parse_cpu_cell_values(
self, property_name: str, dts_content: str
) -> Optional[List[int]]:
"""Parse a CPU-list property accepting legacy and explicit-width cells."""

match = re.search(
rf'{re.escape(property_name)}\s*=\s*'
rf'(?:/bits/\s+(?:32|64)\s*)?<([^>]+)>',
dts_content,
)
if not match:
return None

return self._parse_cell_values(match.group(1))

def _parse_cpus_from_dts(self, dts_content: str) -> CPUAllocation:
"""Parse CPU allocation from DTS content."""

resources_text = self._extract_resources_section(dts_content)
if not resources_text:
raise ParseError("Missing /resources section in DTS")

cpus_match = re.search(r'cpus\s*=\s*<([^>]+)>', resources_text)
if not cpus_match:
available = self._parse_cpu_cell_values('cpus', resources_text)
if available is None:
raise ParseError("Missing 'cpus' property in /resources")

available = [int(x.strip()) for x in cpus_match.group(1).split()]

free_match = re.search(r'cpus-available\s*=\s*<([^>]+)>', resources_text)
available_free = None
if free_match:
available_free = [int(x.strip()) for x in free_match.group(1).split()]

available_free = self._parse_cpu_cell_values('cpus-available', resources_text)
if available:
total = max(available) + 1
else:
Expand Down Expand Up @@ -1177,10 +1273,9 @@ def _parse_instance_resources_from_dts(self, content: str) -> InstanceResources:
resources_text = resources_section.group(1)

# Parse CPUs
cpus_match = re.search(r'cpus\s*=\s*<([^>]+)>', resources_text)
if not cpus_match:
cpus = self._parse_cpu_cell_values('cpus', resources_text)
if cpus is None:
raise ParseError("Missing 'cpus' in resources")
cpus = [int(x.strip()) for x in cpus_match.group(1).split()]

# Parse memory base
memory_base_match = re.search(r'memory-base\s*=\s*<([^>]+)>', resources_text)
Expand All @@ -1202,10 +1297,7 @@ def _parse_instance_resources_from_dts(self, content: str) -> InstanceResources:
devices = [x.strip().lstrip('&') for x in devices_match.group(1).split(',')]

# Parse NUMA nodes (optional)
numa_nodes = None
numa_nodes_match = re.search(r'numa-nodes\s*=\s*<([^>]+)>', resources_text)
if numa_nodes_match:
numa_nodes = [int(x.strip()) for x in numa_nodes_match.group(1).split()]
numa_nodes = self._parse_cpu_cell_values('numa-nodes', resources_text)

# Parse CPU affinity (optional)
cpu_affinity = None
Expand Down Expand Up @@ -1307,12 +1399,10 @@ def _parse_topology_from_dts(self, dts_content: str) -> Optional[TopologySection
"""Parse topology section from DTS content."""

# Look for topology section
topology_section = re.search(r'topology\s*\{([^}]+)\}', dts_content, re.DOTALL)
if not topology_section:
topology_text = self._extract_named_section(dts_content, 'topology')
if not topology_text:
return None

topology_text = topology_section.group(1)

# Parse NUMA nodes from topology section
numa_nodes = self._parse_numa_nodes_from_dts(topology_text)

Expand All @@ -1324,12 +1414,10 @@ def _parse_numa_nodes_from_dts(self, topology_text: str) -> Optional[Dict[int, N
numa_nodes = {}

# Look for numa-nodes subsection
numa_section = re.search(r'numa-nodes\s*\{([^}]+)\}', topology_text, re.DOTALL)
if not numa_section:
numa_text = self._extract_named_section(topology_text, 'numa-nodes')
if not numa_text:
return None

numa_text = numa_section.group(1)

# Find all NUMA node definitions
node_pattern = r'node@(\d+)\s*\{([^}]+)\}'
node_matches = re.finditer(node_pattern, numa_text, re.DOTALL)
Expand All @@ -1356,14 +1444,14 @@ def _parse_numa_nodes_from_dts(self, topology_text: str) -> Optional[Dict[int, N
memory_size = self._parse_hex_value(memory_size_match.group(1))

# Parse CPUs
cpus_match = re.search(r'cpus\s*=\s*<([^>]+)>', node_content)
if cpus_match:
cpus = [int(x.strip()) for x in cpus_match.group(1).split()]
parsed_cpus = self._parse_cpu_cell_values('cpus', node_content)
if parsed_cpus is not None:
cpus = parsed_cpus

# Parse distance matrix (optional)
distance_match = re.search(r'distance-matrix\s*=\s*<([^>]+)>', node_content)
if distance_match:
distances = [int(x.strip()) for x in distance_match.group(1).split()]
distances = self._parse_cell_values(distance_match.group(1))
# Simple distance matrix parsing - would need more sophisticated logic for full matrix
_ = distances # Mark as intentionally unused for now

Expand All @@ -1383,27 +1471,28 @@ def _parse_numa_nodes_from_dts(self, topology_text: str) -> Optional[Dict[int, N

return numa_nodes if numa_nodes else None

def _parse_cpu_topology_from_dts(self, dts_content: str) -> Optional[Dict[int, 'CPUTopology']]:
def _parse_cpu_topology_from_dts(
self, dts_content: str
) -> Optional[Dict[int, 'CPUTopology']]:
"""Parse CPU topology from DTS content."""
from ..models import CPUTopology

topology = {}

# Look for cores section
cores_section = re.search(r'cores\s*\{([^}]+)\}', dts_content, re.DOTALL)
if not cores_section:
cores_text = self._extract_named_section(dts_content, 'cores')
if not cores_text:
return None

cores_text = cores_section.group(1)

# Find all core definitions
core_pattern = r'core@(\d+)\s*\{\s*cpus\s*=\s*<([^>]+)>\s*;\s*\}'
core_pattern = r'core@(\d+)\s*\{([^}]+)\}'
core_matches = re.finditer(core_pattern, cores_text, re.DOTALL)

for match in core_matches:
core_id = int(match.group(1))
cpus_str = match.group(2)
cpus = [int(x.strip()) for x in cpus_str.split()]
cpus = self._parse_cpu_cell_values('cpus', match.group(2))
if cpus is None:
continue

# Create topology entries for each CPU in this core
for i, cpu_id in enumerate(cpus):
Expand Down
Loading