Skip to content

Commit 217e275

Browse files
committed
bcvk-qemu: Add ISO boot mode, serial log, and no-reboot support
In bootc-dev/bootc#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 <walters@verbum.org>
1 parent 3e0a64a commit 217e275

2 files changed

Lines changed: 86 additions & 5 deletions

File tree

crates/bcvk-qemu/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
//!
66
//! # Features
77
//!
8-
//! - **QEMU VM Management**: Launch VMs with direct kernel boot, virtio devices,
9-
//! and automatic resource cleanup
8+
//! - **QEMU VM Management**: Launch VMs via direct kernel boot or ISO boot,
9+
//! with virtio devices and automatic resource cleanup
1010
//! - **VirtioFS Mounts**: Spawn and manage virtiofsd processes for sharing host
1111
//! directories with the guest
1212
//! - **SMBIOS Credentials**: Inject systemd credentials via QEMU SMBIOS interface

crates/bcvk-qemu/src/qemu.rs

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ pub struct ResourceLimits {
118118
pub nice_level: Option<i8>,
119119
}
120120

121-
/// VM boot configuration: direct kernel boot.
121+
/// VM boot configuration.
122122
#[derive(Debug)]
123123
pub enum BootMode {
124124
/// Direct kernel boot (fast, testing-focused).
@@ -139,6 +139,15 @@ pub enum BootMode {
139139
/// VirtIO-FS socket for root filesystem.
140140
virtiofs_socket: Utf8PathBuf,
141141
},
142+
/// Boot from an ISO image (e.g. for Anaconda installer testing).
143+
///
144+
/// The ISO is attached as a CDROM device. Unlike DirectBoot, there is no
145+
/// root virtiofs socket — the installer boots from the ISO and installs
146+
/// to a disk device added via [`QemuConfig::add_virtio_blk_device`].
147+
IsoBoot {
148+
/// Path to the ISO image file.
149+
iso_path: String,
150+
},
142151
}
143152

144153
/// Complete QEMU VM configuration with builder pattern.
@@ -171,6 +180,11 @@ pub struct QemuConfig {
171180
pub enable_console: bool,
172181
/// SMBIOS credentials for systemd.
173182
smbios_credentials: Vec<String>,
183+
/// Path to write serial console output (if set, `-serial file:<path>`
184+
/// is used instead of `-serial none`).
185+
pub serial_log: Option<String>,
186+
/// Prevent automatic reboot (useful for debugging or post-install inspection).
187+
pub no_reboot: bool,
174188

175189
/// Write systemd notifications to this file.
176190
pub systemd_notify: Option<File>,
@@ -200,6 +214,16 @@ impl QemuConfig {
200214
}
201215
}
202216

217+
/// Create a new config for ISO boot (e.g. Anaconda installer).
218+
pub fn new_iso_boot(memory_mb: u32, vcpus: u32, iso_path: String) -> Self {
219+
Self {
220+
memory_mb,
221+
vcpus,
222+
boot_mode: Some(BootMode::IsoBoot { iso_path }),
223+
..Default::default()
224+
}
225+
}
226+
203227
/// Enable vsock support.
204228
pub fn enable_vsock(&mut self) -> Result<()> {
205229
let fd = OpenOptions::new()
@@ -249,6 +273,39 @@ impl QemuConfig {
249273
return Err(eyre!("vCPU count too high: {} (maximum 256)", self.vcpus));
250274
}
251275

276+
// Validate boot mode specifics
277+
match &self.boot_mode {
278+
Some(BootMode::IsoBoot { iso_path }) => {
279+
if iso_path.is_empty() {
280+
return Err(eyre!("ISO path cannot be empty"));
281+
}
282+
if !std::path::Path::new(iso_path).exists() {
283+
return Err(eyre!("ISO image not found: {}", iso_path));
284+
}
285+
// main_virtiofs_config is for the root filesystem in DirectBoot;
286+
// it has no meaning for ISO boot.
287+
if self.main_virtiofs_config.is_some() {
288+
return Err(eyre!(
289+
"main_virtiofs_config is not supported with ISO boot \
290+
(the root filesystem comes from the ISO)"
291+
));
292+
}
293+
}
294+
Some(BootMode::DirectBoot {
295+
kernel_path,
296+
initramfs_path,
297+
..
298+
}) => {
299+
if kernel_path.is_empty() {
300+
return Err(eyre!("Kernel path cannot be empty"));
301+
}
302+
if initramfs_path.is_empty() {
303+
return Err(eyre!("Initramfs path cannot be empty"));
304+
}
305+
}
306+
None => {}
307+
}
308+
252309
// Validate virtiofs mounts
253310
for mount in &self.additional_mounts {
254311
if mount.tag.is_empty() {
@@ -430,6 +487,7 @@ fn spawn(
430487
.map_err(Into::into)
431488
});
432489
}
490+
433491
cmd.args([
434492
"-m",
435493
&memory_arg,
@@ -446,6 +504,10 @@ fn spawn(
446504
"node,memdev=mem",
447505
]);
448506

507+
if config.no_reboot {
508+
cmd.arg("-no-reboot");
509+
}
510+
449511
for (idx, fd) in config.fdset.iter().enumerate() {
450512
let fd_id = 100 + idx as u32; // Start at 100 to avoid conflicts
451513
let set_id = idx + 1; // fdset starts at 1
@@ -498,6 +560,9 @@ fn spawn(
498560
let append_str = kernel_cmdline.join(" ");
499561
cmd.args(["-append", &append_str]);
500562
}
563+
Some(BootMode::IsoBoot { iso_path }) => {
564+
cmd.args(["-cdrom", iso_path]);
565+
}
501566
None => {}
502567
}
503568

@@ -562,8 +627,13 @@ fn spawn(
562627
}
563628
}
564629

565-
// No GUI, and no emulated serial ports by default.
566-
cmd.args(["-serial", "none", "-nographic", "-display", "none"]);
630+
// No GUI; serial console either to a log file or disabled.
631+
if let Some(ref serial_path) = config.serial_log {
632+
cmd.args(["-serial", &format!("file:{}", serial_path)]);
633+
} else {
634+
cmd.args(["-serial", "none"]);
635+
}
636+
cmd.args(["-nographic", "-display", "none"]);
567637

568638
match &config.display_mode {
569639
DisplayMode::None => {
@@ -868,6 +938,17 @@ mod tests {
868938
);
869939
}
870940

941+
#[test]
942+
fn test_iso_boot_config() {
943+
let config = QemuConfig::new_iso_boot(2048, 2, "/test/image.iso".to_string());
944+
assert_eq!(config.memory_mb, 2048);
945+
assert_eq!(config.vcpus, 2);
946+
assert!(matches!(
947+
&config.boot_mode,
948+
Some(BootMode::IsoBoot { iso_path }) if iso_path == "/test/image.iso"
949+
));
950+
}
951+
871952
#[test]
872953
fn test_disk_format() {
873954
assert_eq!(DiskFormat::Raw.as_str(), "raw");

0 commit comments

Comments
 (0)