Skip to content
Merged
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
8 changes: 4 additions & 4 deletions crates/bcvk-qemu/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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};
126 changes: 123 additions & 3 deletions crates/bcvk-qemu/src/qemu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,39 @@ pub struct ResourceLimits {
pub nice_level: Option<i8>,
}

/// VM boot configuration: direct kernel boot.
/// 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 {
/// Direct kernel boot (fast, testing-focused).
Expand All @@ -139,6 +171,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.
Expand All @@ -148,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<BootMode>,
/// Main VirtioFS configuration for root filesystem (handled separately from additional mounts).
pub main_virtiofs_config: Option<VirtiofsConfig>,
Expand All @@ -171,6 +214,11 @@ pub struct QemuConfig {
pub enable_console: bool,
/// SMBIOS credentials for systemd.
smbios_credentials: Vec<String>,
/// Path to write serial console output (if set, `-serial file:<path>`
/// is used instead of `-serial none`).
pub serial_log: Option<String>,
/// Prevent automatic reboot (useful for debugging or post-install inspection).
pub no_reboot: bool,

/// Write systemd notifications to this file.
pub systemd_notify: Option<File>,
Expand Down Expand Up @@ -200,6 +248,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()
Expand Down Expand Up @@ -249,6 +307,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() {
Expand Down Expand Up @@ -430,6 +521,12 @@ fn spawn(
.map_err(Into::into)
});
}

// Set machine type (auto-detected or explicit)
if let Some(machine) = config.machine_type.resolve() {
cmd.args(["-machine", machine]);
}

cmd.args([
"-m",
&memory_arg,
Expand All @@ -446,6 +543,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
Expand Down Expand Up @@ -498,6 +599,9 @@ fn spawn(
let append_str = kernel_cmdline.join(" ");
cmd.args(["-append", &append_str]);
}
Some(BootMode::IsoBoot { iso_path }) => {
cmd.args(["-cdrom", iso_path]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, we boot from ISO and boot the ISO's bootloader. That requires firmware. Do we need add UEFI boot or just keep BIOS by default? Thanks.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes...the messy thing here is that supporting uefi adds more nontrivial code into our qemu.rs which we don't technically need right now.

There's just this giant tension between this case versus libvirt (and systemd-vmspawn) among other cases.

}
None => {}
}

Expand Down Expand Up @@ -562,8 +666,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 => {
Expand Down Expand Up @@ -868,6 +977,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");
Expand Down