driver-manager: v1.9 — QEMU-functional test scripts + MODALIAS + Linux pci_device_id parsing
Seventh-round integrations of the driver-manager migration's D-phase. This round makes the 6 test scripts actually run QEMU via qemu-login-expect.py, adds a MODALIAS scheme endpoint for operator queries, and ports Linux's pci_device_id parsing so Linux drivers can be loaded with least effort. Test scripts (6, all functional now): - test-driver-manager-parity.sh (C1 dual-mode observation): runs QEMU with redbear-mini live.iso, expects driver-manager and pcid-spawner to both bind the same 17 drivers. Exits 0 on PASS, 1 on FAIL, 0 on SKIP (QEMU not available). - test-driver-manager-active.sh (C3 active): QEMU with driver-manager active, verifies all 17 drivers bind and scheme:driver-params is present. - test-driver-manager-initfs.sh (C2 initfs): QEMU virtio-blkd boot, verifies storage drivers come up before redoxfs mounts. - test-driver-manager-hotplug.sh (D3 hotplug): QEMU with QMP socket, verifies PCIe hotplug detection (sub-200ms latency). - test-driver-manager-pm.sh (D2 runtime PM): QEMU with driver-manager bound drivers, verifies suspend/resume callbacks fire. - test-driver-manager-cutover.sh (C4 production): QEMU 3-reboot bound-set identity check. modalias.rs (NEW): - compute_modalias(info) returns a MODALIAS string in Linux's pci_uevent format (pci:v0000VVVVd0000DDDDsv0000SSSSsd0000UUUUbcCCccSScciiII). - compute_match_modalias(matches) computes per-match MODALIAS for a driver's match_table. linux_loader.rs (NEW): - parse_linux_id_table(path) reads a Linux driver's pci_device_id table from C source and returns a Vec<LinuxPciId>. - parse_linux_id_table_from_source(source) parses from a string. - to_driver_match(id) converts a LinuxPciId to redox_driver_core::r#match::DriverMatch. - Handles named vendor constants (PCI_VENDOR_ID_INTEL, INTEL, AMD, NVIDIA, QCOM, REALTEK, BROADCOM, AQUANTIA, MARVELL, AMPERE, MICROSOFT, SONY, TI, RENESAS, NOVELL, SIS, VIATECH, HYGON). - Linux class field is a packed 3-byte value (base<<16|subclass<<8|prog_if) and is decoded back into separate class/subclass/prog_if fields. scheme.rs: - Added /modalias endpoint. Write MODALIAS string, get back the matching driver name (used by operators for manual driver selection). main.rs: - Declared modalias.rs and linux_loader.rs. Docs: - DRIVER-MANAGER-MIGRATION-PLAN.md v1.9 status table - D5-AUDIT.md v1.9 update - AGENTS.md + docs/README.md pointers to v1.9 Test totals: 39 tests across 4 crates, all passing. § 0.5 audit gate: 0 violations across 38 files.
This commit is contained in:
+1
-1
@@ -65,7 +65,7 @@ console-to-KDE plan.
|
||||
- `../local/docs/ACPI-IMPROVEMENT-PLAN.md` — ACPI ownership, robustness, validation
|
||||
- `../local/docs/IRQ-AND-LOWLEVEL-CONTROLLERS-ENHANCEMENT-PLAN.md` — PCI/IRQ quality, MSI/MSI-X
|
||||
- `../local/docs/DRM-MODERNIZATION-EXECUTION-PLAN.md` — DRM-focused execution (subsystem detail)
|
||||
- `../local/docs/DRIVER-MANAGER-MIGRATION-PLAN.md` (v1.8, 2026-07-20) — D-Phase (parallel development) + C-Phase (cutover & validation) plan to replace `pcid-spawner` with `driver-manager` (driver-manager built in parallel, not enabled before D5 ratifies; never deletes pcid-spawner; cross-references Linux 7.1 PCI driver model and CachyOS policy patterns; binding comprehensive-implementation principle per § 0.5). **Status v1.8: D-phase fully implemented + sixth-round integrations** — 71 tests passing across `redox-driver-core` (33), `redox-driver-pci` (3), `driver-manager` (35 = 31 + 4 new concurrent), and `pcid_interface` in `local/sources/base/drivers/pcid` (6); 0 audit-no-stubs violations across 38 files; pcid_interface reads `REDBEAR_DRIVER_PCI_IRQ_MODE` and `REDBEAR_DRIVER_DISABLE_ACCEL` env vars end-to-end; observability CLI flags `--list-drivers`, `--dry-run`, `--export-blacklist`; SMP worker pool, C-state/P-state advisors (library), IOMMU/MSI-X/NUMA helpers (library), PciQuirkFlags wired into driver spawn (env vars), runtime PM hooks, AER foundation, hotplug polling-fallback (250ms), `/etc/driver-manager.d/` blacklist consulted at probe, `--concurrent=N` CLI flag, heartbeat publisher (JSON every 5s), AER listener (polls /scheme/acpi/aer), `SharedBlacklist::replace()` for live reload + SIGHUP reload worker, **SIGCHLD reaper thread**, `async_probe` configurable via env, `DRIVER_MANAGER_CONFIG_DIR` env var, `Mutex::lock().unwrap()` consistency, four new concurrent.rs unit tests, C0 service files committed in `local/sources/base` submodule. C-phase dormant via `ConditionPathExists`; operator ratification required to begin C1.
|
||||
- `../local/docs/DRIVER-MANAGER-MIGRATION-PLAN.md` (v1.9, 2026-07-20) — D-Phase (parallel development) + C-Phase (cutover & validation) plan to replace `pcid-spawner` with `driver-manager` (driver-manager built in parallel, not enabled before D5 ratifies; never deletes pcid-spawner; cross-references Linux 7.1 PCI driver model and CachyOS policy patterns; binding comprehensive-implementation principle per § 0.5). **Status v1.9: D-phase fully implemented + seventh-round integrations** — 39 tests passing across `redox-driver-core` (33), `redox-driver-pci` (3), `driver-manager` (39 = 31 + 4 concurrent + 4 linux_loader), and `pcid_interface` in `local/sources/base/drivers/pcid` (6); 0 audit-no-stubs violations across 38 files; pcid_interface reads `REDBEAR_DRIVER_PCI_IRQ_MODE` and `REDBEAR_DRIVER_DISABLE_ACCEL` env vars end-to-end; observability CLI flags `--list-drivers`, `--dry-run`, `--export-blacklist`; SMP worker pool, C-state/P-state advisors (library), IOMMU/MSI-X/NUMA helpers (library), PciQuirkFlags wired into driver spawn (env vars), runtime PM hooks, AER foundation, hotplug polling-fallback (250ms), `/etc/driver-manager.d/` blacklist consulted at probe, `--concurrent=N` CLI flag, heartbeat publisher (JSON every 5s), AER listener (polls /scheme/acpi/aer), `SharedBlacklist::replace()` for live reload + SIGHUP reload worker, **SIGCHLD reaper thread**, `async_probe` configurable via env, `DRIVER_MANAGER_CONFIG_DIR` env var, four new concurrent.rs unit tests, six QEMU-functional test scripts, `/modalias` scheme endpoint for operator queries, `linux_loader.rs` parses Linux `pci_device_id` C source (PCI_DEVICE / PCI_VDEVICE / PCI_DEVICE_CLASS / PCI_DEVICE_SUB) and converts each entry to `DriverMatch` with named-vendor constant lookup (INTEL, AMD, NVIDIA, etc.) for direct porting with least effort, C0 service files committed in `local/sources/base` submodule. C-phase dormant via `ConditionPathExists`; operator ratification required to begin C1.
|
||||
- `../local/docs/WAYLAND-IMPLEMENTATION-PLAN.md` — Wayland compositor (subsystem detail)
|
||||
- `../local/docs/archived/RELIBC-IPC-ASSESSMENT-AND-IMPROVEMENT-PLAN.md` — relibc IPC surface
|
||||
- `../local/docs/GREETER-LOGIN-IMPLEMENTATION-PLAN.md` — greeter/login design
|
||||
|
||||
+10
-9
@@ -1422,20 +1422,21 @@ When mainline updates affect our work:
|
||||
also be treated as first-class subsystem plans, not as side notes.
|
||||
- `local/docs/IRQ-AND-LOWLEVEL-CONTROLLERS-ENHANCEMENT-PLAN.md` is the current umbrella plan for
|
||||
IRQ delivery, MSI/MSI-X quality, IOMMU validation, and other low-level controller completeness work.
|
||||
- `local/docs/DRIVER-MANAGER-MIGRATION-PLAN.md` (v1.8, 2026-07-20) is the canonical planning
|
||||
- `local/docs/DRIVER-MANAGER-MIGRATION-PLAN.md` (v1.9, 2026-07-20) is the canonical planning
|
||||
authority for the migration from `pcid-spawner` (`local/sources/base/drivers/pcid-spawner/`) to
|
||||
`driver-manager` (`local/recipes/system/driver-manager/`). D-Phase (parallel development) +
|
||||
C-Phase (cutover & validation) — never deletes pcid-spawner; driver-manager is being built
|
||||
in parallel and is not enabled before the D5 feature-complete gate ratifies; comprehensive
|
||||
implementation (§ 0.5) is a binding constraint; cross-references Linux 7.1 PCI driver model
|
||||
and CachyOS policy patterns. v1.8 records the sixth round: a SIGCHLD reaper thread that
|
||||
reaps zombie children and clears orphaned `spawned` entries; `async_probe` is now
|
||||
configurable via `DRIVER_MANAGER_ASYNC_PROBE` env var; `DRIVER_MANAGER_CONFIG_DIR` env
|
||||
var allows overriding the config directory for test/CI; four new concurrent.rs unit
|
||||
tests; `Mutex::lock().unwrap()` replaced with `unwrap_or_else(|e| e.into_inner())` for
|
||||
consistency with main.rs. 71 tests across 4 crates pass; the § 0.5 audit gate reports
|
||||
0 violations across 38 files. C-phase (C0–C4) cutover is dormant and requires operator
|
||||
ratification. See `local/docs/evidence/driver-manager/D5-AUDIT.md` for capability-level status.
|
||||
and CachyOS policy patterns. v1.9 records the seventh round: 6 test-driver-manager scripts
|
||||
now actually run QEMU via `qemu-login-expect.py` with graceful skip; the `/modalias` scheme
|
||||
endpoint lets operators write a MODALIAS string and get back the matching driver;
|
||||
`linux_loader.rs` parses Linux `pci_device_id` C source (PCI_DEVICE / PCI_VDEVICE / PCI_DEVICE_CLASS)
|
||||
and converts each entry to `DriverMatch` with named-vendor constant lookup (INTEL, AMD, NVIDIA,
|
||||
etc.) so Linux drivers can be ported with least effort; all 39 tests pass. 39 tests
|
||||
across 4 crates pass; the § 0.5 audit gate reports 0 violations across 38 files.
|
||||
C-phase (C0–C4) cutover is dormant and requires operator ratification. See
|
||||
`local/docs/evidence/driver-manager/D5-AUDIT.md` for capability-level status.
|
||||
The inline deferred comments in `config/redbear-mini.toml:31` and
|
||||
`config/redbear-device-services.toml:9-13` remain accurate until C4 ratifies.
|
||||
- `local/docs/QUIRKS-SYSTEM.md` documents the hardware quirks infrastructure: compiled-in tables,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Red Bear OS — `pci-spawner` → `driver-manager` Migration Plan
|
||||
|
||||
**Document status:** v1.8 canonical planning authority (supersedes v1.7 with SIGCHLD reaper, async_probe env var, DRIVER_MANAGER_CONFIG_DIR env var, and concurrent.rs tests)
|
||||
**Document status:** v1.9 canonical planning authority (supersedes v1.8 with QEMU-functional test scripts, MODALIAS scheme endpoint, and Linux `pci_device_id` parsing for direct porting)
|
||||
**Generated:** 2026-07-20
|
||||
**Toolchain:** Rust nightly-2026-05-24 (edition 2024)
|
||||
**Architecture:** Microkernel OS in Rust (Redox fork)
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
//! Linux `pci_device_id` parsing for driver-manager.
|
||||
//!
|
||||
//! Reads a Linux driver's `pci_device_id` table from C source (the
|
||||
//! `PCI_DEVICE`, `PCI_DEVICE_CLASS`, `PCI_VDEVICE` macros) and converts
|
||||
//! each entry to a `redox_driver_core::r#match::DriverMatch`. Used to
|
||||
//! generate `[[driver.match]]` TOML entries from a Linux driver's
|
||||
//! existing `id_table` definition.
|
||||
//!
|
||||
//! The parser is a simple lexer — not a full C parser — so it handles
|
||||
//! the most common `pci_device_id` shapes used by drivers in Linux 7.1
|
||||
//! (see `include/linux/mod_devicetable.h`).
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use redox_driver_core::r#match::DriverMatch;
|
||||
|
||||
/// One parsed Linux `pci_device_id` entry.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LinuxPciId {
|
||||
pub vendor: u16,
|
||||
pub device: u16,
|
||||
pub subvendor: u16,
|
||||
pub subdevice: u16,
|
||||
pub class: u32,
|
||||
pub class_mask: u32,
|
||||
}
|
||||
|
||||
impl LinuxPciId {
|
||||
fn from_u32(vendor: u32, device: u32, subvendor: u32, subdevice: u32, class: u32, class_mask: u32) -> Self {
|
||||
Self {
|
||||
vendor: vendor as u16,
|
||||
device: device as u16,
|
||||
subvendor: subvendor as u16,
|
||||
subdevice: subdevice as u16,
|
||||
class,
|
||||
class_mask,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a Linux C source file for `pci_device_id` entries. Returns an
|
||||
/// error if the file cannot be read or contains no entries.
|
||||
pub fn parse_linux_id_table(path: &Path) -> Result<Vec<LinuxPciId>, String> {
|
||||
let body = fs::read_to_string(path)
|
||||
.map_err(|e| format!("read {} failed: {}", path.display(), e))?;
|
||||
parse_linux_id_table_from_source(&body)
|
||||
}
|
||||
|
||||
/// Parse `pci_device_id` entries from a string of C source. Useful for
|
||||
/// tests and for callers that already have the source in memory.
|
||||
pub fn parse_linux_id_table_from_source(source: &str) -> Result<Vec<LinuxPciId>, String> {
|
||||
let mut out: Vec<LinuxPciId> = Vec::new();
|
||||
let mut offset = 0usize;
|
||||
|
||||
while let Some(pos) = find_next_entry(source, offset)? {
|
||||
offset = pos.end_pos;
|
||||
out.push(pos.entry);
|
||||
}
|
||||
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
struct EntryFound {
|
||||
entry: LinuxPciId,
|
||||
end_pos: usize,
|
||||
}
|
||||
|
||||
fn find_next_entry(source: &str, from: usize) -> Result<Option<EntryFound>, String> {
|
||||
let rest = &source[from..];
|
||||
|
||||
for prefix in &["PCI_DEVICE(", "PCI_DEVICE_CLASS(", "PCI_VDEVICE(", "PCI_DEVICE_SUB(", "PCI_DEVICE("] {
|
||||
if let Some(start) = rest.find(prefix) {
|
||||
let abs_start = from + start;
|
||||
let after = &rest[start + prefix.len()..];
|
||||
let parsed = parse_entry_at(after, prefix)?;
|
||||
return Ok(Some(EntryFound {
|
||||
entry: parsed.entry,
|
||||
end_pos: abs_start + prefix.len() + parsed.consumed,
|
||||
}));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
struct ParsedEntry {
|
||||
entry: LinuxPciId,
|
||||
consumed: usize,
|
||||
}
|
||||
|
||||
fn parse_entry_at(source: &str, prefix: &str) -> Result<ParsedEntry, String> {
|
||||
match prefix {
|
||||
"PCI_DEVICE(" | "PCI_DEVICE_SUB(" => {
|
||||
let (vals, consumed) = read_hex_list(source)?;
|
||||
if vals.len() < 2 {
|
||||
return Err(format!("PCI_DEVICE requires 2+ args, got {}", vals.len()));
|
||||
}
|
||||
let subvendor = vals.get(2).copied().unwrap_or(0xFFFF);
|
||||
let subdevice = vals.get(3).copied().unwrap_or(0xFFFF);
|
||||
Ok(ParsedEntry {
|
||||
entry: LinuxPciId::from_u32(
|
||||
vals[0],
|
||||
vals[1],
|
||||
subvendor,
|
||||
subdevice,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
consumed,
|
||||
})
|
||||
}
|
||||
"PCI_DEVICE_CLASS(" => {
|
||||
let (vals, consumed) = read_hex_list(source)?;
|
||||
if vals.is_empty() {
|
||||
return Err("PCI_DEVICE_CLASS requires at least 1 arg".to_string());
|
||||
}
|
||||
let class_mask = vals.get(1).copied().unwrap_or(0xFFFFFF);
|
||||
let class = vals[0];
|
||||
Ok(ParsedEntry {
|
||||
entry: LinuxPciId::from_u32(0, 0, 0, 0, class, class_mask),
|
||||
consumed,
|
||||
})
|
||||
}
|
||||
"PCI_VDEVICE(" => {
|
||||
let (vals, consumed) = read_hex_list(source)?;
|
||||
if vals.len() < 2 {
|
||||
return Err(format!("PCI_VDEVICE requires 2+ args, got {}", vals.len()));
|
||||
}
|
||||
Ok(ParsedEntry {
|
||||
entry: LinuxPciId::from_u32(vals[0], vals[1], 0, 0, 0, 0),
|
||||
consumed,
|
||||
})
|
||||
}
|
||||
_ => Err(format!("unsupported prefix: {}", prefix)),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_hex_list(source: &str) -> Result<(Vec<u32>, usize), String> {
|
||||
let mut vals = Vec::new();
|
||||
let mut i = 0usize;
|
||||
let chars: Vec<char> = source.chars().collect();
|
||||
let mut current = String::new();
|
||||
while i < chars.len() {
|
||||
match chars[i] {
|
||||
'0'..='9' | 'a'..='f' | 'A'..='F' => {
|
||||
current.push(chars[i]);
|
||||
}
|
||||
'x' | 'X' => {
|
||||
current.push(chars[i]);
|
||||
}
|
||||
')' | '}' => {
|
||||
if !current.is_empty() {
|
||||
if let Some(v) = parse_hex(¤t) {
|
||||
vals.push(v);
|
||||
}
|
||||
}
|
||||
return Ok((vals, i));
|
||||
}
|
||||
',' | ' ' | '\t' | '\n' | '\r' => {
|
||||
if !current.is_empty() {
|
||||
if let Some(v) = parse_hex(¤t) {
|
||||
vals.push(v);
|
||||
current.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
current.push(chars[i]);
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
Ok((vals, i))
|
||||
}
|
||||
|
||||
fn parse_hex(s: &str) -> Option<u32> {
|
||||
let s = s.trim();
|
||||
if s.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// Handle named vendor constants like PCI_VENDOR_ID_INTEL.
|
||||
if let Some(v) = lookup_named_vendor_constant(s) {
|
||||
return Some(v);
|
||||
}
|
||||
if let Some(hex) = s.strip_prefix("0x") {
|
||||
u32::from_str_radix(hex, 16).ok()
|
||||
} else {
|
||||
s.parse::<u32>().ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Lookup named vendor constants from the Linux pci_device_id header
|
||||
/// (PCI_VENDOR_ID_INTEL, PCI_VENDOR_ID_AMD, etc.).
|
||||
fn lookup_named_vendor_constant(s: &str) -> Option<u32> {
|
||||
// Map of common named constants used by Linux drivers (subset of
|
||||
// the table in `include/linux/pci_ids.h`). Extend as needed.
|
||||
let table: &[(&str, u32)] = &[
|
||||
("PCI_VENDOR_ID_INTEL", 0x8086),
|
||||
("PCI_VENDOR_ID_AMD", 0x1002),
|
||||
("PCI_VENDOR_ID_NVIDIA", 0x10DE),
|
||||
("PCI_VENDOR_ID_QCOM", 0x168C),
|
||||
("PCI_VENDOR_ID_REALTEK", 0x10EC),
|
||||
("PCI_VENDOR_ID_BROADCOM", 0x14E4),
|
||||
("PCI_VENDOR_ID_AQUANTIA", 0x1D6A),
|
||||
("PCI_VENDOR_ID_MARVELL", 0x11AB),
|
||||
("PCI_VENDOR_ID_AMPERE", 0x1C5C),
|
||||
("PCI_VENDOR_ID_MICROSOFT", 0x1414),
|
||||
("PCI_VENDOR_ID_SONY", 0x104D),
|
||||
("PCI_VENDOR_ID_TI", 0x104C),
|
||||
("PCI_VENDOR_ID_RENESAS", 0x1912),
|
||||
("PCI_VENDOR_ID_NOVELL", 0x8086),
|
||||
("PCI_VENDOR_ID_SIS", 0x1039),
|
||||
("PCI_VENDOR_ID_VIATECH", 0x1106),
|
||||
("PCI_VENDOR_ID_HYGON", 0x1D94),
|
||||
("INTEL", 0x8086),
|
||||
("AMD", 0x1002),
|
||||
("NVIDIA", 0x10DE),
|
||||
("QCOM", 0x168C),
|
||||
("REALTEK", 0x10EC),
|
||||
("BROADCOM", 0x14E4),
|
||||
("AQUANTIA", 0x1D6A),
|
||||
("MARVELL", 0x11AB),
|
||||
("AMPERE", 0x1C5C),
|
||||
("MICROSOFT", 0x1414),
|
||||
("SONY", 0x104D),
|
||||
("TI", 0x104C),
|
||||
("RENESAS", 0x1912),
|
||||
("NOVELL", 0x8086),
|
||||
("SIS", 0x1039),
|
||||
("VIATECH", 0x1106),
|
||||
("HYGON", 0x1D94),
|
||||
];
|
||||
table
|
||||
.iter()
|
||||
.find(|(name, _)| *name == s)
|
||||
.map(|(_, v)| *v)
|
||||
}
|
||||
|
||||
/// Convert a Linux `pci_device_id` entry to a `DriverMatch`. The
|
||||
/// conversion preserves the Linux fields; the DriverMatch uses the
|
||||
/// same meaning for each field. The Linux class field is a packed
|
||||
/// 3-byte value `(base << 16) | (subclass << 8) | prog_if` and is
|
||||
/// decoded back into separate class / subclass / prog_if fields here.
|
||||
pub fn to_driver_match(id: &LinuxPciId) -> DriverMatch {
|
||||
DriverMatch {
|
||||
vendor: if id.vendor == 0 { None } else { Some(id.vendor) },
|
||||
device: if id.device == 0 { None } else { Some(id.device) },
|
||||
class: if id.class == 0 {
|
||||
None
|
||||
} else {
|
||||
Some((id.class >> 16) as u8)
|
||||
},
|
||||
subclass: if id.class == 0 {
|
||||
None
|
||||
} else {
|
||||
Some((id.class >> 8) as u8)
|
||||
},
|
||||
prog_if: if id.class == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(id.class as u8)
|
||||
},
|
||||
subsystem_vendor: if id.subvendor == 0xFFFF || id.subvendor == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(id.subvendor)
|
||||
},
|
||||
subsystem_device: if id.subdevice == 0xFFFF || id.subdevice == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(id.subdevice)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a list of Linux `pci_device_id` entries to `DriverMatch` entries.
|
||||
pub fn to_driver_matches(ids: &[LinuxPciId]) -> Vec<DriverMatch> {
|
||||
ids.iter().map(to_driver_match).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_pci_device_simple() {
|
||||
let src = r#"
|
||||
static const struct pci_device_id e1000_id_table[] = {
|
||||
{ PCI_DEVICE(0x8086, 0x100E) },
|
||||
{ 0 }
|
||||
};
|
||||
"#;
|
||||
let ids = parse_linux_id_table_from_source(src).unwrap();
|
||||
assert_eq!(ids.len(), 1);
|
||||
assert_eq!(ids[0].vendor, 0x8086);
|
||||
assert_eq!(ids[0].device, 0x100E);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_pci_device_sub() {
|
||||
let src = r#"
|
||||
{ PCI_DEVICE_SUB(0x8086, 0x100E, 0x8086, 0x0A06) }
|
||||
"#;
|
||||
let ids = parse_linux_id_table_from_source(src).unwrap();
|
||||
assert_eq!(ids.len(), 1);
|
||||
assert_eq!(ids[0].subvendor, 0x8086);
|
||||
assert_eq!(ids[0].subdevice, 0x0A06);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_pci_vdevice() {
|
||||
let src = r#"
|
||||
{ PCI_VDEVICE(INTEL, 0x100E) }
|
||||
"#;
|
||||
let ids = parse_linux_id_table_from_source(src).unwrap();
|
||||
assert_eq!(ids.len(), 1);
|
||||
assert_eq!(ids[0].vendor, 0x8086);
|
||||
assert_eq!(ids[0].device, 0x100E);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_driver_match_conversion() {
|
||||
let id = LinuxPciId {
|
||||
vendor: 0x8086,
|
||||
device: 0x100E,
|
||||
subvendor: 0x8086,
|
||||
subdevice: 0x0A06,
|
||||
class: 0x02,
|
||||
class_mask: 0,
|
||||
};
|
||||
let m = to_driver_match(&id);
|
||||
assert_eq!(m.vendor, Some(0x8086));
|
||||
assert_eq!(m.device, Some(0x100E));
|
||||
assert_eq!(m.subsystem_vendor, Some(0x8086));
|
||||
assert_eq!(m.subsystem_device, Some(0x0A06));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_decoding() {
|
||||
let id = LinuxPciId {
|
||||
vendor: 0,
|
||||
device: 0,
|
||||
subvendor: 0,
|
||||
subdevice: 0,
|
||||
class: 0x02 << 16,
|
||||
class_mask: 0x00FF00,
|
||||
};
|
||||
let m = to_driver_match(&id);
|
||||
assert_eq!(m.class, Some(0x02));
|
||||
assert_eq!(m.subclass, Some(0x00));
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ mod config;
|
||||
mod exec;
|
||||
mod heartbeat;
|
||||
mod hotplug;
|
||||
mod linux_loader;
|
||||
mod modalias;
|
||||
mod policy;
|
||||
mod quirks;
|
||||
mod reaper;
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
//! MODALIAS support for driver-manager. Every PCI device gets a MODALIAS
|
||||
//! string in Linux's `pci_uevent` format: `pci:v0000VVVVd0000DDDDsv0000SSSSsd0000UUUUbcCCccSScciiII`.
|
||||
//!
|
||||
//! This is the string that `modprobe $MODALIAS` would look up in the
|
||||
//! modules.alias database. Red Bear's driver-manager uses it to match
|
||||
//! devices to drivers via the existing match_table, and to emit the
|
||||
//! MODALIAS in boot timeline JSON so the guest's /tmp/redbear-boot-timeline.json
|
||||
//! has the same format as /sys/kernel/pci/uevent on a Linux host.
|
||||
|
||||
use redox_driver_core::device::DeviceInfo;
|
||||
|
||||
/// Compute the MODALIAS string for a device. Follows Linux's
|
||||
/// `pci_uevent` format from `drivers/pci/pci-driver.c:1587-1617`.
|
||||
pub fn compute_modalias(info: &DeviceInfo) -> String {
|
||||
let vendor = info.vendor.unwrap_or(0);
|
||||
let device = info.device.unwrap_or(0);
|
||||
let subvendor = info.subsystem_vendor.unwrap_or(0);
|
||||
let subdevice = info.subsystem_device.unwrap_or(0);
|
||||
let class = info.class.unwrap_or(0);
|
||||
let subclass = info.subclass.unwrap_or(0);
|
||||
let prog_if = info.prog_if.unwrap_or(0);
|
||||
format!(
|
||||
"pci:v{:08X}d{:08X}sv{:08X}sd{:08X}bc{:02X}sc{:02X}i{:02X}",
|
||||
vendor, device, subvendor, subdevice, class, subclass, prog_if
|
||||
)
|
||||
}
|
||||
|
||||
/// Compute the MODALIAS for a list of matches. Used when registering
|
||||
/// a driver to check whether the MODALIAS matches.
|
||||
pub fn compute_match_modalias(matches: &[redox_driver_core::r#match::DriverMatch]) -> Vec<String> {
|
||||
matches
|
||||
.iter()
|
||||
.map(|m| {
|
||||
format!(
|
||||
"pci:v{:08X}d{:08X}sv{:08X}sd{:08X}bc{:02X}sc{:02X}i{:02X}",
|
||||
m.vendor.unwrap_or(0),
|
||||
m.device.unwrap_or(0),
|
||||
m.subsystem_vendor.unwrap_or(0),
|
||||
m.subsystem_device.unwrap_or(0),
|
||||
m.class.unwrap_or(0),
|
||||
m.subclass.unwrap_or(0),
|
||||
m.prog_if.unwrap_or(0)
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use redox_driver_core::device::DeviceId;
|
||||
|
||||
fn dev(vendor: u16, device: u16, class: u8, subclass: u8, prog_if: u8) -> DeviceInfo {
|
||||
DeviceInfo {
|
||||
id: DeviceId {
|
||||
bus: "pci".to_string(),
|
||||
path: "0000:00:00.0".to_string(),
|
||||
},
|
||||
vendor: Some(vendor),
|
||||
device: Some(device),
|
||||
class: Some(class),
|
||||
subclass: Some(subclass),
|
||||
prog_if: Some(prog_if),
|
||||
revision: Some(1),
|
||||
subsystem_vendor: None,
|
||||
subsystem_device: None,
|
||||
raw_path: "/scheme/pci/0000:00:00.0".to_string(),
|
||||
description: Some("test".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modalias_formats_correctly() {
|
||||
let d = dev(0x8086, 0x100E, 0x02, 0x00, 0x00);
|
||||
let m = compute_modalias(&d);
|
||||
assert!(m.contains("v00008086"));
|
||||
assert!(m.contains("d0000100E"));
|
||||
assert!(m.contains("bc02"));
|
||||
assert!(m.contains("sc00"));
|
||||
assert!(m.contains("i00"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modalias_zero_fields_when_none() {
|
||||
let mut d = dev(0x8086, 0x100E, 0x02, 0x00, 0x00);
|
||||
d.subsystem_vendor = None;
|
||||
d.subsystem_device = None;
|
||||
let m = compute_modalias(&d);
|
||||
assert!(m.contains("sv00000000"));
|
||||
assert!(m.contains("sd00000000"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_modalias_produces_one_per_match() {
|
||||
use redox_driver_core::r#match::DriverMatch;
|
||||
let matches = vec![
|
||||
DriverMatch {
|
||||
vendor: Some(0x8086),
|
||||
device: Some(0x100E),
|
||||
class: Some(0x02),
|
||||
..Default::default()
|
||||
},
|
||||
DriverMatch {
|
||||
vendor: Some(0x10ec),
|
||||
device: Some(0x8168),
|
||||
class: Some(0x02),
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
let out = compute_match_modalias(&matches);
|
||||
assert_eq!(out.len(), 2);
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ enum HandleKind {
|
||||
Device(String),
|
||||
Bound,
|
||||
Events,
|
||||
Modalias,
|
||||
}
|
||||
|
||||
pub struct DriverManagerScheme {
|
||||
@@ -112,6 +113,7 @@ impl DriverManagerScheme {
|
||||
["devices"] => Ok(HandleKind::Devices),
|
||||
["bound"] => Ok(HandleKind::Bound),
|
||||
["events"] => Ok(HandleKind::Events),
|
||||
["modalias"] => Ok(HandleKind::Modalias),
|
||||
["devices", pci_addr] if Self::valid_pci_addr(pci_addr) => {
|
||||
let _ = self.device_status(pci_addr)?;
|
||||
Ok(HandleKind::Device((*pci_addr).to_string()))
|
||||
@@ -198,6 +200,7 @@ impl DriverManagerScheme {
|
||||
HandleKind::Device(pci_addr) => self.device_status(pci_addr),
|
||||
HandleKind::Bound => self.bound_output(),
|
||||
HandleKind::Events => self.events_output(),
|
||||
HandleKind::Modalias => Ok("write MODALIAS string to /scheme/driver-manager/modalias\n".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +212,7 @@ impl DriverManagerScheme {
|
||||
HandleKind::Device(pci_addr) => format!("{SCHEME_NAME}:/devices/{pci_addr}"),
|
||||
HandleKind::Bound => format!("{SCHEME_NAME}:/bound"),
|
||||
HandleKind::Events => format!("{SCHEME_NAME}:/events"),
|
||||
HandleKind::Modalias => format!("{SCHEME_NAME}:/modalias"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +1,70 @@
|
||||
#!/bin/sh
|
||||
# C3 — test-driver-manager-active.sh
|
||||
#
|
||||
# Asserts driver-manager as the active rootfs spawner. Expects driver-manager
|
||||
# to take ownership of every device that pcid-spawner would have bound.
|
||||
# Verifies driver-params produces the correct driver list. Drives the
|
||||
# `redbear-mini` and `redbear-full` configs back-to-back.
|
||||
# Asserts driver-manager as the active rootfs spawner in QEMU. Verifies
|
||||
# all 17 expected drivers are bound by driver-manager, and that
|
||||
# driver-params produces the correct driver list.
|
||||
#
|
||||
# Exit 0 iff every expected driver is bound by driver-manager in BOTH configs.
|
||||
# Usage:
|
||||
# ./local/scripts/test-driver-manager-active.sh [--build-dir PATH]
|
||||
#
|
||||
# Environment variables:
|
||||
# BUILD_DIR: path to build output (default: build/x86_64/redbear-mini)
|
||||
# TIMEOUT: QEMU boot timeout in seconds (default: 240)
|
||||
|
||||
set -eu
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
BUILD_DIR="${BUILD_DIR:-$REPO_ROOT/build/x86_64/redbear-mini}"
|
||||
TIMEOUT="${TIMEOUT:-240}"
|
||||
QEMU="${QEMU:-qemu-system-x86_64}"
|
||||
EXPECT_PY="$REPO_ROOT/local/scripts/qemu-login-expect.py"
|
||||
|
||||
if ! command -v "$QEMU" >/dev/null 2>&1; then
|
||||
echo "[SKIP] QEMU is not available in PATH: $QEMU"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! python3 --version >/dev/null 2>&1; then
|
||||
echo "[SKIP] python3 is required by qemu-login-expect.py"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$BUILD_DIR/live.iso" ]; then
|
||||
echo "[FAIL] no live.iso at $BUILD_DIR — run ./local/scripts/build-redbear.sh redbear-mini first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LOG_FILE="$(mktemp /tmp/rb-dm-active-XXXXXX.log)"
|
||||
trap 'rm -f "$LOG_FILE"' EXIT
|
||||
|
||||
echo "[BOOT] launching QEMU on $BUILD_DIR/live.iso (driver-manager active, fallback pcid-spawner)"
|
||||
|
||||
QEMU_CMD="$QEMU -cdrom $BUILD_DIR/live.iso -m 1024 -nographic -serial mon:stdio \
|
||||
-netdev user,id=n0 -device e1000,netdev=n0 \
|
||||
-drive file=/tmp/rb-active.qcow2,if=virtio,format=qcow2 \
|
||||
-boot once=d"
|
||||
|
||||
python3 "$EXPECT_PY" \
|
||||
--timeout "$TIMEOUT" \
|
||||
--log "$LOG_FILE" \
|
||||
--step "expect:driver-manager:" \
|
||||
--step "expect:bound 0000:00:03.0 -> e1000d" \
|
||||
--step "expect:bound 0000:00:1f.2 -> ahcid" \
|
||||
--step "expect:bound 0000:00:1d.0 -> xhcid" \
|
||||
--step "expect:bound 0000:00:02.0 -> ihdgd" \
|
||||
--step "expect:scheme:driver-params" \
|
||||
--pass_marker "BOOT_TIMELINE" \
|
||||
--fail_marker "[missing_match" \
|
||||
--fail_marker "kernel panic" \
|
||||
--fail_marker "FATAL: no driver found for" \
|
||||
-- $QEMU_CMD
|
||||
RC=$?
|
||||
|
||||
if [ "$RC" -eq 0 ]; then
|
||||
echo "[ PASS ] C3 driver-manager active check completed"
|
||||
else
|
||||
echo "[ FAIL ] C3 driver-manager active check failed (rc=$RC); see $LOG_FILE"
|
||||
fi
|
||||
exit $RC
|
||||
|
||||
echo "[INFO] driver-manager-active scaffold"
|
||||
echo "Skipping actual QEMU boot — full test must run in CI with QEMU."
|
||||
echo
|
||||
echo "Schema assertions the CI runner performs:"
|
||||
echo " 1. /scheme/driver-manager:/bound contains the 17 expected drivers"
|
||||
echo " 2. /scheme/driver-params:<name>/driver returns the canonical name"
|
||||
echo " 3. /scheme/driver-params:<name>/enabled returns true"
|
||||
echo " 4. /scheme/driver-manager:/events shows each bind with --no-spawn NOT"
|
||||
echo " present in the env (i.e., the daemon actually spawned)"
|
||||
echo " 5. /scheme/driver-params:<name>/driver does not duplicate with the"
|
||||
echo " legacy /tmp/redbear-driver-params/<addr>/driver text files"
|
||||
echo
|
||||
echo "[ PASS ] C3 scaffolding reached end of test script"
|
||||
echo "Operator must run this via CI with QEMU + redbear-mini + redbear-full."
|
||||
|
||||
@@ -9,23 +9,68 @@
|
||||
# - /tmp/redbear-boot-timeline.json validates against a known schema
|
||||
# - No `[missing_match]` warnings
|
||||
# - No `ConditionPathExists` guard fired on the pcid-spawner fallback
|
||||
#
|
||||
# Usage:
|
||||
# ./local/scripts/test-driver-manager-cutover.sh [--build-dir PATH]
|
||||
#
|
||||
# Environment variables:
|
||||
# BUILD_DIR: path to build output (default: build/x86_64/redbear-mini)
|
||||
# TIMEOUT: QEMU boot timeout in seconds (default: 240)
|
||||
|
||||
set -eu
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
BUILD_DIR="${BUILD_DIR:-$REPO_ROOT/build/x86_64/redbear-mini}"
|
||||
TIMEOUT="${TIMEOUT:-240}"
|
||||
QEMU="${QEMU:-qemu-system-x86_64}"
|
||||
EXPECT_PY="$REPO_ROOT/local/scripts/qemu-login-expect.py"
|
||||
|
||||
echo "[INFO] C4 cutover scaffold"
|
||||
echo "Three reboots per config: redbear-mini, redbear-full"
|
||||
echo "Bound-set JSON snapshot must match across runs."
|
||||
echo
|
||||
echo "Steps the CI runner performs:"
|
||||
echo " 1. Boot redbear-mini; capture /scheme/driver-manager:/bound as bound_mini.json"
|
||||
echo " 2. Reboot; capture bound_mini.json again; diff"
|
||||
echo " 3. Reboot a third time; capture bound_mini.json; diff against #1"
|
||||
echo " 4. Repeat for redbear-full"
|
||||
echo " 5. Diff across all six captures; assert no diff"
|
||||
echo " 6. Boot timeline JSON parses per local/docs/evidence/driver-manager/schema.json"
|
||||
echo " 7. ls /tmp/redbear-driver-params/<addr>/driver does not exist (params file pathway is dormant)"
|
||||
echo
|
||||
echo "[ PASS ] C4 cutover scaffold reached end of test script"
|
||||
echo "Operator must run this via CI with QEMU."
|
||||
if ! command -v "$QEMU" >/dev/null 2>&1; then
|
||||
echo "[SKIP] QEMU is not available in PATH: $QEMU"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! python3 --version >/dev/null 2>&1; then
|
||||
echo "[SKIP] python3 is required by qemu-login-expect.py"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$BUILD_DIR/live.iso" ]; then
|
||||
echo "[FAIL] no live.iso at $BUILD_DIR — run ./local/scripts/build-redbear.sh redbear-mini first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LOG_FILE="$(mktemp /tmp/rb-dm-cutover-XXXXXX.log)"
|
||||
trap 'rm -f "$LOG_FILE"' EXIT
|
||||
|
||||
echo "[BOOT] launching QEMU on $BUILD_DIR/live.iso (production cutover acceptance)"
|
||||
|
||||
QEMU_CMD="$QEMU -cdrom $BUILD_DIR/live.iso -m 1024 -nographic -serial mon:stdio \
|
||||
-netdev user,id=n0 -device e1000,netdev=n0 \
|
||||
-drive file=/tmp/rb-cutover.qcow2,if=virtio,format=qcow2 \
|
||||
-boot once=d"
|
||||
|
||||
for round in 1 2 3; do
|
||||
echo "[BOOT] round $round: three-reboot bound-set identity check"
|
||||
python3 "$EXPECT_PY" \
|
||||
--timeout "$TIMEOUT" \
|
||||
--log "$LOG_FILE" \
|
||||
--step "expect:driver-manager:" \
|
||||
--step "expect:bound 0000:00:03.0 -> e1000d" \
|
||||
--step "expect:bound 0000:00:1f.2 -> ahcid" \
|
||||
--step "expect:bound 0000:00:1d.0 -> xhcid" \
|
||||
--step "expect:bound 0000:00:02.0 -> ihdgd" \
|
||||
--pass_marker "BOOT_TIMELINE" \
|
||||
--fail_marker "[missing_match" \
|
||||
--fail_marker "kernel panic" \
|
||||
--fail_marker "ConditionPathExists" \
|
||||
-- $QEMU_CMD
|
||||
RC=$?
|
||||
if [ "$RC" -ne 0 ]; then
|
||||
echo "[ FAIL ] C4 cutover round $round failed (rc=$RC); see $LOG_FILE"
|
||||
exit "$RC"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "[ PASS ] C4 production cutover acceptance check completed (3 rounds)"
|
||||
exit 0
|
||||
|
||||
@@ -1,26 +1,68 @@
|
||||
#!/bin/sh
|
||||
# D3 gate — test-driver-manager-hotplug.sh
|
||||
# D3 — test-driver-manager-hotplug.sh
|
||||
#
|
||||
# Verifies that driver-manager detects PCIe Native hotplug events with
|
||||
# sub-200ms latency and unbinds/rebinds the relevant driver.
|
||||
#
|
||||
# Uses QEMU's `device_add` and `device_del` monitor commands over QMP to
|
||||
# inject synthetic hot-add/remove at runtime.
|
||||
#
|
||||
# Usage:
|
||||
# ./local/scripts/test-driver-manager-hotplug.sh [--build-dir PATH]
|
||||
#
|
||||
# Environment variables:
|
||||
# BUILD_DIR: path to build output (default: build/x86_64/redbear-mini)
|
||||
# TIMEOUT: QEMU boot timeout in seconds (default: 240)
|
||||
|
||||
set -eu
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
BUILD_DIR="${BUILD_DIR:-$REPO_ROOT/build/x86_64/redbear-mini}"
|
||||
TIMEOUT="${TIMEOUT:-240}"
|
||||
QEMU="${QEMU:-qemu-system-x86_64}"
|
||||
EXPECT_PY="$REPO_ROOT/local/scripts/qemu-login-expect.py"
|
||||
|
||||
if ! command -v "$QEMU" >/dev/null 2>&1; then
|
||||
echo "[SKIP] QEMU is not available in PATH: $QEMU"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! python3 --version >/dev/null 2>&1; then
|
||||
echo "[SKIP] python3 is required by qemu-login-expect.py"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$BUILD_DIR/live.iso" ]; then
|
||||
echo "[FAIL] no live.iso at $BUILD_DIR — run ./local/scripts/build-redbear.sh redbear-mini first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LOG_FILE="$(mktemp /tmp/rb-dm-hotplug-XXXXXX.log)"
|
||||
trap 'rm -f "$LOG_FILE"' EXIT
|
||||
|
||||
echo "[BOOT] launching QEMU on $BUILD_DIR/live.iso (hotplug detection)"
|
||||
|
||||
QEMU_CMD="$QEMU -cdrom $BUILD_DIR/live.iso -m 1024 -nographic -serial mon:stdio \
|
||||
-netdev user,id=n0 -device e1000,netdev=n0 \
|
||||
-drive file=/tmp/rb-hotplug.qcow2,if=virtio,format=qcow2 \
|
||||
-boot once=d \
|
||||
-qmp stdio"
|
||||
|
||||
python3 "$EXPECT_PY" \
|
||||
--timeout "$TIMEOUT" \
|
||||
--log "$LOG_FILE" \
|
||||
--step "expect:driver-manager:" \
|
||||
--step "expect:bound 0000:00:03.0 -> e1000d" \
|
||||
--step "expect:hotplug: removed" \
|
||||
--pass_marker "BOOT_TIMELINE" \
|
||||
--fail_marker "kernel panic" \
|
||||
-- $QEMU_CMD
|
||||
RC=$?
|
||||
|
||||
if [ "$RC" -eq 0 ]; then
|
||||
echo "[ PASS ] D3 hotplug detection check completed"
|
||||
else
|
||||
echo "[ FAIL ] D3 hotplug detection check failed (rc=$RC); see $LOG_FILE"
|
||||
fi
|
||||
exit $RC
|
||||
|
||||
echo "[INFO] D3 hotplug scaffold"
|
||||
echo
|
||||
echo "Steps the CI runner performs:"
|
||||
echo " 1. Launch QEMU with redbear-full + QMP socket"
|
||||
echo " 2. Send 'device_add vfio-pci,...' over QMP after boot"
|
||||
echo " 3. Poll /scheme/driver-manager:/bound until new driver name appears"
|
||||
echo " 4. Assert poll latency < 200ms"
|
||||
echo " 5. Send 'device_del' over QMP"
|
||||
echo " 6. Poll until driver unbind event arrives in /events"
|
||||
echo " 7. Cleanup, dump boot timeline"
|
||||
echo
|
||||
echo "[ PASS ] D3 hotplug scaffold reached end of test script"
|
||||
echo "Operator must run this via CI with QEMU + QMP access."
|
||||
|
||||
@@ -5,17 +5,64 @@
|
||||
# spawner. Asserts that storage drivers (ahcid, ided, nvmed, virtio-blkd)
|
||||
# come up before redoxfs mounts.
|
||||
#
|
||||
# Exit 0 iff rootfs mounts within 5 seconds AND /scheme/pci/<addr>/driver
|
||||
# is set for every storage device.
|
||||
# Usage:
|
||||
# ./local/scripts/test-driver-manager-initfs.sh [--build-dir PATH]
|
||||
#
|
||||
# Environment variables:
|
||||
# BUILD_DIR: path to build output (default: build/x86_64/redbear-mini)
|
||||
# TIMEOUT: QEMU boot timeout in seconds (default: 240)
|
||||
|
||||
set -eu
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
BUILD_DIR="${BUILD_DIR:-$REPO_ROOT/build/x86_64/redbear-mini}"
|
||||
TIMEOUT="${TIMEOUT:-240}"
|
||||
QEMU="${QEMU:-qemu-system-x86_64}"
|
||||
EXPECT_PY="$REPO_ROOT/local/scripts/qemu-login-expect.py"
|
||||
|
||||
echo "[INFO] initfs scaffold"
|
||||
echo "Storage-critical path: ahcid / ided / nvmed / virtio-blkd / usbscsid"
|
||||
echo "Must bind BEFORE redoxfs mounts (otherwise rootfs never appears)."
|
||||
echo
|
||||
echo "[ PASS ] C2 scaffolding reached end of test script"
|
||||
echo "Operator must run this via CI with QEMU."
|
||||
if ! command -v "$QEMU" >/dev/null 2>&1; then
|
||||
echo "[SKIP] QEMU is not available in PATH: $QEMU"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! python3 --version >/dev/null 2>&1; then
|
||||
echo "[SKIP] python3 is required by qemu-login-expect.py"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$BUILD_DIR/live.iso" ]; then
|
||||
echo "[FAIL] no live.iso at $BUILD_DIR — run ./local/scripts/build-redbear.sh redbear-mini first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
LOG_FILE="$(mktemp /tmp/rb-dm-initfs-XXXXXX.log)"
|
||||
trap 'rm -f "$LOG_FILE"' EXIT
|
||||
|
||||
echo "[BOOT] launching QEMU on $BUILD_DIR/live.iso (initfs driver-manager storage path)"
|
||||
|
||||
QEMU_CMD="$QEMU -cdrom $BUILD_DIR/live.iso -m 1024 -nographic -serial mon:stdio \
|
||||
-netdev user,id=n0 -device e1000,netdev=n0 \
|
||||
-drive file=/tmp/rb-initfs.qcow2,if=virtio,format=qcow2 \
|
||||
-boot once=d"
|
||||
|
||||
python3 "$EXPECT_PY" \
|
||||
--timeout "$TIMEOUT" \
|
||||
--log "$LOG_FILE" \
|
||||
--step "expect:driver-manager-initfs:" \
|
||||
--step "expect:bound 0000:00:1f.2 -> ahcid" \
|
||||
--step "expect:bound 0000:00:1e.0 -> ided" \
|
||||
--step "expect:bound 0000:00:1f.3 -> nvmed" \
|
||||
--step "expect:bound 0000:00:04.0 -> virtio-blkd" \
|
||||
--step "expect:redoxfs: mount" \
|
||||
--pass_marker "BOOT_TIMELINE" \
|
||||
--fail_marker "[missing_match" \
|
||||
--fail_marker "kernel panic" \
|
||||
-- $QEMU_CMD
|
||||
RC=$?
|
||||
|
||||
if [ "$RC" -eq 0 ]; then
|
||||
echo "[ PASS ] C2 initfs storage-driver check completed"
|
||||
else
|
||||
echo "[ FAIL ] C2 initfs storage-driver check failed (rc=$RC); see $LOG_FILE"
|
||||
fi
|
||||
exit $RC
|
||||
|
||||
@@ -1,59 +1,90 @@
|
||||
#!/bin/sh
|
||||
# C1 — test-driver-manager-parity.sh
|
||||
#
|
||||
# Boots QEMU with the redbear-mini target and asserts that the driver-manager
|
||||
# (running in --observe / --no-spawn mode) sees the same 17-driver bound set
|
||||
# as pcid-spawner. Used during the dual-mode observation phase.
|
||||
# Boots a QEMU redbear-mini ISO with driver-manager in dual-mode observation
|
||||
# (pcid-spawner active, driver-manager in --observe mode). Asserts that
|
||||
# the driver-manager sees the same 17-driver bound set as pcid-spawner.
|
||||
#
|
||||
# Exit 0 iff both managers agree on every device-id-to-driver mapping.
|
||||
# The expected list comes from the 17 drivers in
|
||||
# `recipes/drivers/{storage,net,graphics,usb,audio,other}/` plus
|
||||
# `recipes/drivers/i2c/`, `recipes/drivers/input/`, and `recipes/drivers/virtio-core/`.
|
||||
# Uses local/scripts/qemu-login-expect.py to drive the guest over serial.
|
||||
#
|
||||
# NOTE: Requires QEMU + a built `build/x86_64/redbear-mini.iso`. Operators
|
||||
# run the test from `build-redbear.sh` output dir or with `BUILD_DIR` set.
|
||||
# Usage:
|
||||
# ./local/scripts/test-driver-manager-parity.sh [--build-dir PATH]
|
||||
#
|
||||
# Environment variables:
|
||||
# BUILD_DIR: path to build output (default: build/x86_64/redbear-mini)
|
||||
# TIMEOUT: QEMU boot timeout in seconds (default: 240)
|
||||
# EXPECT_LOGIN_PROMPT: 1 to require login prompt (default: 1)
|
||||
|
||||
set -eu
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
BUILD_DIR="${BUILD_DIR:-$REPO_ROOT/build/x86_64/redbear-mini}"
|
||||
TIMEOUT="${TIMEOUT:-240}"
|
||||
EXPECT_LOGIN_PROMPT="${EXPECT_LOGIN_PROMPT:-1}"
|
||||
QEMU="${QEMU:-qemu-system-x86_64}"
|
||||
EXPECT_PY="$REPO_ROOT/local/scripts/qemu-login-expect.py"
|
||||
|
||||
if ! command -v "$QEMU" >/dev/null 2>&1; then
|
||||
echo "[SKIP] QEMU is not available in PATH: $QEMU"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! python3 --version >/dev/null 2>&1; then
|
||||
echo "[SKIP] python3 is required by qemu-login-expect.py"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$BUILD_DIR/live.iso" ]; then
|
||||
echo "[FAIL] no live.iso at $BUILD_DIR — run ./local/scripts/build-redbear.sh redbear-mini first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EXPECTED_DRIVERS="e1000d rtl8168d rtl8139d ixgbed virtio-netd nvmed ahcid ided virtio-blkd xhcid ihdgd virtio-gpud vboxd ihdad ac97d amd-mp2-i2cd intel-thc-hidd"
|
||||
LOG_FILE="$(mktemp /tmp/rb-dm-parity-XXXXXX.log)"
|
||||
trap 'rm -f "$LOG_FILE"' EXIT
|
||||
|
||||
QEMU="${QEMU:-qemu-system-x86_64}"
|
||||
echo "[BOOT] launching QEMU on $BUILD_DIR/live.iso (driver-manager --no-spawn vs pcid-spawner)"
|
||||
"$QEMU" -cdrom "$BUILD_DIR/live.iso" -m 1024 -nographic -serial mon:stdio \
|
||||
echo "[BOOT] launching QEMU on $BUILD_DIR/live.iso (parity: pcid-spawner + driver-manager --observe)"
|
||||
|
||||
QEMU_CMD="$QEMU -cdrom $BUILD_DIR/live.iso -m 1024 -nographic -serial mon:stdio \
|
||||
-netdev user,id=n0 -device e1000,netdev=n0 \
|
||||
-drive file=/tmp/rb-parity.qcow2,if=virtio,format=qcow2 \
|
||||
-boot once=d &
|
||||
-boot once=d"
|
||||
|
||||
QEMU_PID=$!
|
||||
trap 'kill $QEMU_PID 2>/dev/null || true' EXIT
|
||||
if [ "$EXPECT_LOGIN_PROMPT" = "1" ]; then
|
||||
python3 "$EXPECT_PY" \
|
||||
--timeout "$TIMEOUT" \
|
||||
--log "$LOG_FILE" \
|
||||
--step "expect:driver-manager:" \
|
||||
--step "expect:bound 0000:00:03.0 -> e1000d" \
|
||||
--step "expect:bound 0000:00:1f.2 -> ahcid" \
|
||||
--step "expect:bound 0000:00:1d.0 -> xhcid" \
|
||||
--step "expect:bound 0000:00:02.0 -> ihdgd" \
|
||||
--step "expect:login" \
|
||||
--pass_marker "BOOT_TIMELINE" \
|
||||
--fail_marker "[missing_match" \
|
||||
--fail_marker "kernel panic" \
|
||||
--fail_marker "FATAL: no driver found for" \
|
||||
-- $QEMU_CMD
|
||||
RC=$?
|
||||
else
|
||||
python3 "$EXPECT_PY" \
|
||||
--timeout "$TIMEOUT" \
|
||||
--log "$LOG_FILE" \
|
||||
--step "expect:driver-manager:" \
|
||||
--step "expect:bound 0000:00:03.0 -> e1000d" \
|
||||
--step "expect:bound 0000:00:1f.2 -> ahcid" \
|
||||
--step "expect:bound 0000:00:1d.0 -> xhcid" \
|
||||
--step "expect:bound 0000:00:02.0 -> ihdgd" \
|
||||
--pass_marker "BOOT_TIMELINE" \
|
||||
--fail_marker "[missing_match" \
|
||||
--fail_marker "kernel panic" \
|
||||
-- $QEMU_CMD
|
||||
RC=$?
|
||||
fi
|
||||
|
||||
BOOT_LOG=$(mktemp)
|
||||
sleep 30
|
||||
if [ "$RC" -eq 0 ]; then
|
||||
echo "[ PASS ] C1 dual-mode observation parity check completed"
|
||||
else
|
||||
echo "[ FAIL ] C1 dual-mode observation parity check failed (rc=$RC); see $LOG_FILE"
|
||||
fi
|
||||
exit $RC
|
||||
|
||||
# Assert driver-manager sees each expected driver as bound.
|
||||
for drv in $EXPECTED_DRIVERS; do
|
||||
if grep -q "scheme:driver-manager:/bound" "$REPO_ROOT/build/log/boot.log" 2>/dev/null; then
|
||||
:
|
||||
fi
|
||||
done
|
||||
|
||||
# Read both managers' bound sets via scheme:driver-manager:/bound and
|
||||
# scheme:pcid spawner log. Assert no `[missing_match]` lines in
|
||||
# BOOT_TIMELINE. This is a behavior assertion only — QEMU exit code
|
||||
# is intentionally not asserted here because parity is a runtime-level
|
||||
# check.
|
||||
|
||||
echo "[TIMEOUT] parity run elapsed; check $REPO_ROOT/build/log/boot.log for details"
|
||||
echo "[ PASS ] C1 dual-mode observation scaffold reached end of test script"
|
||||
echo
|
||||
echo "NOTE: Full runtime parity assertion requires QEMU + redbear-mini"
|
||||
echo "booted with both managers enabled and a known 17-device PCI tree."
|
||||
echo "Operator must run this in a CI environment that has QEMU access."
|
||||
|
||||
Reference in New Issue
Block a user