From b56f18593c1dd6d9897dd89aa26eb1e96255cd82 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Fri, 27 Feb 2026 15:29:52 -0500 Subject: [PATCH 1/2] bcvk-qemu: Add ISO boot mode, serial log, and no-reboot support In https://github.com/bootc-dev/bootc/pull/2018 we want to run an Anaconda install - but reusing host content mounted via virtiofs, and we keep running into this same problem of while we can do it with libvirt/virt-install, there's some nontrivial glue needed there, and there's *so many* advantages to directly running qemu (especially lifecycle binding, low level control) and we already are carrying qemu code here anyways. So extend our qemu logic to support ISO images, along with: - serial_log field for directing serial console to a file - no_reboot flag to prevent automatic reboot Assisted-by: OpenCode (claude-opus-4-5-20250114) Signed-off-by: Colin Walters --- crates/bcvk-qemu/src/lib.rs | 4 +- crates/bcvk-qemu/src/qemu.rs | 87 ++++++++++++++++++++++++++++++++++-- 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/crates/bcvk-qemu/src/lib.rs b/crates/bcvk-qemu/src/lib.rs index 7d811e8..f8900e8 100644 --- a/crates/bcvk-qemu/src/lib.rs +++ b/crates/bcvk-qemu/src/lib.rs @@ -5,8 +5,8 @@ //! //! # Features //! -//! - **QEMU VM Management**: Launch VMs with direct kernel boot, virtio devices, -//! and automatic resource cleanup +//! - **QEMU VM Management**: Launch VMs via direct kernel boot or ISO boot, +//! with virtio devices and automatic resource cleanup //! - **VirtioFS Mounts**: Spawn and manage virtiofsd processes for sharing host //! directories with the guest //! - **SMBIOS Credentials**: Inject systemd credentials via QEMU SMBIOS interface diff --git a/crates/bcvk-qemu/src/qemu.rs b/crates/bcvk-qemu/src/qemu.rs index 475d721..ce10426 100644 --- a/crates/bcvk-qemu/src/qemu.rs +++ b/crates/bcvk-qemu/src/qemu.rs @@ -118,7 +118,7 @@ pub struct ResourceLimits { pub nice_level: Option, } -/// VM boot configuration: direct kernel boot. +/// VM boot configuration. #[derive(Debug)] pub enum BootMode { /// Direct kernel boot (fast, testing-focused). @@ -139,6 +139,15 @@ pub enum BootMode { /// VirtIO-FS socket for root filesystem. virtiofs_socket: Utf8PathBuf, }, + /// Boot from an ISO image (e.g. for Anaconda installer testing). + /// + /// The ISO is attached as a CDROM device. Unlike DirectBoot, there is no + /// root virtiofs socket — the installer boots from the ISO and installs + /// to a disk device added via [`QemuConfig::add_virtio_blk_device`]. + IsoBoot { + /// Path to the ISO image file. + iso_path: String, + }, } /// Complete QEMU VM configuration with builder pattern. @@ -171,6 +180,11 @@ pub struct QemuConfig { pub enable_console: bool, /// SMBIOS credentials for systemd. smbios_credentials: Vec, + /// Path to write serial console output (if set, `-serial file:` + /// is used instead of `-serial none`). + pub serial_log: Option, + /// Prevent automatic reboot (useful for debugging or post-install inspection). + pub no_reboot: bool, /// Write systemd notifications to this file. pub systemd_notify: Option, @@ -200,6 +214,16 @@ impl QemuConfig { } } + /// Create a new config for ISO boot (e.g. Anaconda installer). + pub fn new_iso_boot(memory_mb: u32, vcpus: u32, iso_path: String) -> Self { + Self { + memory_mb, + vcpus, + boot_mode: Some(BootMode::IsoBoot { iso_path }), + ..Default::default() + } + } + /// Enable vsock support. pub fn enable_vsock(&mut self) -> Result<()> { let fd = OpenOptions::new() @@ -249,6 +273,39 @@ impl QemuConfig { return Err(eyre!("vCPU count too high: {} (maximum 256)", self.vcpus)); } + // Validate boot mode specifics + match &self.boot_mode { + Some(BootMode::IsoBoot { iso_path }) => { + if iso_path.is_empty() { + return Err(eyre!("ISO path cannot be empty")); + } + if !std::path::Path::new(iso_path).exists() { + return Err(eyre!("ISO image not found: {}", iso_path)); + } + // main_virtiofs_config is for the root filesystem in DirectBoot; + // it has no meaning for ISO boot. + if self.main_virtiofs_config.is_some() { + return Err(eyre!( + "main_virtiofs_config is not supported with ISO boot \ + (the root filesystem comes from the ISO)" + )); + } + } + Some(BootMode::DirectBoot { + kernel_path, + initramfs_path, + .. + }) => { + if kernel_path.is_empty() { + return Err(eyre!("Kernel path cannot be empty")); + } + if initramfs_path.is_empty() { + return Err(eyre!("Initramfs path cannot be empty")); + } + } + None => {} + } + // Validate virtiofs mounts for mount in &self.additional_mounts { if mount.tag.is_empty() { @@ -430,6 +487,7 @@ fn spawn( .map_err(Into::into) }); } + cmd.args([ "-m", &memory_arg, @@ -446,6 +504,10 @@ fn spawn( "node,memdev=mem", ]); + if config.no_reboot { + cmd.arg("-no-reboot"); + } + for (idx, fd) in config.fdset.iter().enumerate() { let fd_id = 100 + idx as u32; // Start at 100 to avoid conflicts let set_id = idx + 1; // fdset starts at 1 @@ -498,6 +560,9 @@ fn spawn( let append_str = kernel_cmdline.join(" "); cmd.args(["-append", &append_str]); } + Some(BootMode::IsoBoot { iso_path }) => { + cmd.args(["-cdrom", iso_path]); + } None => {} } @@ -562,8 +627,13 @@ fn spawn( } } - // No GUI, and no emulated serial ports by default. - cmd.args(["-serial", "none", "-nographic", "-display", "none"]); + // No GUI; serial console either to a log file or disabled. + if let Some(ref serial_path) = config.serial_log { + cmd.args(["-serial", &format!("file:{}", serial_path)]); + } else { + cmd.args(["-serial", "none"]); + } + cmd.args(["-nographic", "-display", "none"]); match &config.display_mode { DisplayMode::None => { @@ -868,6 +938,17 @@ mod tests { ); } + #[test] + fn test_iso_boot_config() { + let config = QemuConfig::new_iso_boot(2048, 2, "/test/image.iso".to_string()); + assert_eq!(config.memory_mb, 2048); + assert_eq!(config.vcpus, 2); + assert!(matches!( + &config.boot_mode, + Some(BootMode::IsoBoot { iso_path }) if iso_path == "/test/image.iso" + )); + } + #[test] fn test_disk_format() { assert_eq!(DiskFormat::Raw.as_str(), "raw"); From f077fcf892e9f002d76c5ca63ac809c050be1884 Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Mon, 2 Mar 2026 21:00:06 -0500 Subject: [PATCH 2/2] bcvk-qemu: Add MachineType for architecture-appropriate defaults Per previous commit we're still carrying direct qemu code, and we really do want to use the modern default machine types. This came up as part of deduplicating our qemu logic with that generated for custom bootc testing. This mirrors the machine types used by bcvk's libvirt integration (see crates/kit/src/arch.rs) for consistency between direct QEMU and libvirt code paths. Assisted-by: OpenCode (claude-opus-4-5@20251101) Signed-off-by: Colin Walters --- crates/bcvk-qemu/src/lib.rs | 4 ++-- crates/bcvk-qemu/src/qemu.rs | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/crates/bcvk-qemu/src/lib.rs b/crates/bcvk-qemu/src/lib.rs index f8900e8..91cbffe 100644 --- a/crates/bcvk-qemu/src/lib.rs +++ b/crates/bcvk-qemu/src/lib.rs @@ -53,8 +53,8 @@ pub use credentials::{ }; pub use qemu::{ - BootMode, DiskFormat, DisplayMode, NetworkMode, QemuConfig, ResourceLimits, RunningQemu, - VirtioBlkDevice, VirtioSerialOut, VirtiofsMount, VHOST_VSOCK, + BootMode, DiskFormat, DisplayMode, MachineType, NetworkMode, QemuConfig, ResourceLimits, + RunningQemu, VirtioBlkDevice, VirtioSerialOut, VirtiofsMount, VHOST_VSOCK, }; pub use virtiofsd::{spawn_virtiofsd_async, validate_virtiofsd_config, VirtiofsConfig}; diff --git a/crates/bcvk-qemu/src/qemu.rs b/crates/bcvk-qemu/src/qemu.rs index ce10426..7760a23 100644 --- a/crates/bcvk-qemu/src/qemu.rs +++ b/crates/bcvk-qemu/src/qemu.rs @@ -118,6 +118,38 @@ pub struct ResourceLimits { pub nice_level: Option, } +/// QEMU machine type selection. +#[derive(Debug, Clone, Default)] +pub enum MachineType { + /// Auto-detect based on host architecture (recommended). + #[default] + Auto, + /// Use a specific machine type string (e.g., "q35", "pc-q35-9.2"). + Explicit(String), +} + +impl MachineType { + /// Resolve to a QEMU `-machine` argument for the current host. + /// + /// Returns `None` for unknown architectures, letting QEMU use its default. + pub fn resolve(&self) -> Option<&str> { + // xref: https://github.com/coreos/coreos-assembler/blob/main/mantle/platform/qemu.go + match self { + Self::Auto => match std::env::consts::ARCH { + "x86_64" => Some("q35"), + // gic-version=max selects the best available GIC for the host + "aarch64" => Some("virt,gic-version=max"), + "s390x" => Some("s390-ccw-virtio"), + // kvm-type=HV ensures bare metal KVM, not user mode + // ic-mode=xics for interrupt controller + "powerpc64" => Some("pseries,kvm-type=HV,ic-mode=xics"), + _ => None, + }, + Self::Explicit(name) => Some(name.as_str()), + } + } +} + /// VM boot configuration. #[derive(Debug)] pub enum BootMode { @@ -157,6 +189,8 @@ pub struct QemuConfig { pub memory_mb: u32, /// Number of vCPUs (1-256). pub vcpus: u32, + /// Machine type (default: auto-detect based on host architecture). + pub machine_type: MachineType, boot_mode: Option, /// Main VirtioFS configuration for root filesystem (handled separately from additional mounts). pub main_virtiofs_config: Option, @@ -488,6 +522,11 @@ fn spawn( }); } + // Set machine type (auto-detected or explicit) + if let Some(machine) = config.machine_type.resolve() { + cmd.args(["-machine", machine]); + } + cmd.args([ "-m", &memory_arg,