Skip to content
Open
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
59 changes: 57 additions & 2 deletions crates/bcvk-qemu/src/qemu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ pub struct VirtioBlkDevice {
pub serial: String,
/// Disk image format.
pub format: DiskFormat,
/// Mount as read-only.
pub readonly: bool,
}

/// VM display and console configuration.
Expand Down Expand Up @@ -224,6 +226,9 @@ pub struct QemuConfig {
pub systemd_notify: Option<File>,

vhost_fd: Option<File>,

/// fw_cfg entries for passing config files to the guest
fw_cfg_entries: Vec<(String, Utf8PathBuf)>,
}

impl QemuConfig {
Expand Down Expand Up @@ -365,11 +370,23 @@ impl QemuConfig {
disk_file: String,
serial: String,
format: DiskFormat,
) -> &mut Self {
self.add_virtio_blk_device_ro(disk_file, serial, format, false)
}

/// Add a virtio-blk device with specified format and readonly flag.
pub fn add_virtio_blk_device_ro(
&mut self,
disk_file: String,
serial: String,
format: DiskFormat,
readonly: bool,
) -> &mut Self {
self.virtio_blk_devices.push(VirtioBlkDevice {
disk_file,
serial,
format,
readonly,
});
self
}
Expand Down Expand Up @@ -440,6 +457,13 @@ impl QemuConfig {
};
self
}

/// Add a fw_cfg entry to pass a file to the guest.
/// The file will be accessible in the guest via the fw_cfg interface.
pub fn add_fw_cfg(&mut self, name: String, file_path: Utf8PathBuf) -> &mut Self {
self.fw_cfg_entries.push((name, file_path));
self
}
}

/// Allocate a unique VSOCK CID.
Expand Down Expand Up @@ -560,13 +584,19 @@ fn spawn(
// Add virtio-blk block devices
for (idx, blk_device) in config.virtio_blk_devices.iter().enumerate() {
let drive_id = format!("drive{}", idx);
let readonly_flag = if blk_device.readonly {
",readonly=on"
} else {
""
};
cmd.args([
"-drive",
&format!(
"file={},format={},if=none,id={}",
"file={},format={},if=none,id={}{}",
blk_device.disk_file,
blk_device.format.as_str(),
drive_id
drive_id,
readonly_flag
),
"-device",
&format!(
Expand Down Expand Up @@ -723,6 +753,11 @@ fn spawn(
cmd.args(["-smbios", &format!("type=11,value={}", credential)]);
}

// Add fw_cfg entries
for (name, file_path) in &config.fw_cfg_entries {
cmd.args(["-fw_cfg", &format!("name={},file={}", name, file_path)]);
}

// Configure stdio based on display mode
match &config.display_mode {
DisplayMode::Console => {
Expand Down Expand Up @@ -993,4 +1028,24 @@ mod tests {
assert_eq!(DiskFormat::Raw.as_str(), "raw");
assert_eq!(DiskFormat::Qcow2.as_str(), "qcow2");
}

#[test]
fn test_fw_cfg_entry() {
let mut config = QemuConfig::new_direct_boot(
1024,
1,
"/test/kernel".to_string(),
"/test/initramfs".to_string(),
"/test/socket".into(),
);
config.add_fw_cfg(
"opt/com.coreos/config".to_string(),
"/test/ignition.json".into(),
);

// Test that the fw_cfg entry is created correctly
assert_eq!(config.fw_cfg_entries.len(), 1);
assert_eq!(config.fw_cfg_entries[0].0, "opt/com.coreos/config");
assert_eq!(config.fw_cfg_entries[0].1.as_str(), "/test/ignition.json");
}
}
1 change: 1 addition & 0 deletions crates/integration-tests/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod tests {
pub mod libvirt_verb;
pub mod mount_feature;
pub mod run_ephemeral;
pub mod run_ephemeral_ignition;
pub mod run_ephemeral_ssh;
pub mod to_disk;
pub mod varlink;
Expand Down
228 changes: 228 additions & 0 deletions crates/integration-tests/src/tests/run_ephemeral_ignition.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
//! Integration tests for Ignition config injection

use color_eyre::Result;
use integration_tests::integration_test;
use xshell::cmd;

use std::fs;
use tempfile::TempDir;

use camino::Utf8Path;

use crate::{get_bck_command, shell, INTEGRATION_TEST_LABEL};

/// Fedora CoreOS image that supports Ignition
const FCOS_IMAGE: &str = "quay.io/fedora/fedora-coreos:stable";

/// Test that Ignition config is injected via fw_cfg and accessible in the guest
///
/// This test verifies:
/// 1. Ignition config file is passed to QEMU via fw_cfg
/// 2. The config is readable at /sys/firmware/qemu_fw_cfg/by_name/opt/com.coreos/config/raw
fn test_run_ephemeral_ignition_fw_cfg_accessible() -> Result<()> {
let sh = shell()?;
let bck = get_bck_command()?;
let label = INTEGRATION_TEST_LABEL;

// Pull FCOS image first
cmd!(sh, "podman pull -q {FCOS_IMAGE}").run()?;

// Create a temporary Ignition config
let temp_dir = TempDir::new()?;
let config_path = Utf8Path::from_path(temp_dir.path())
.expect("temp dir is not utf8")
.join("config.ign");

// Minimal valid Ignition config (v3.3.0 for FCOS)
let ignition_config = r#"{
"ignition": {
"version": "3.3.0"
},
"storage": {
"files": [
{
"path": "/etc/ignition-test-marker",
"contents": {
"source": "data:,ignition-applied"
},
"mode": 420
}
]
}
}"#;
fs::write(&config_path, ignition_config)?;

// Run ephemeral VM and check if fw_cfg is accessible
// We just verify the config is present in fw_cfg, not that it gets applied
// (FCOS won't apply it in ephemeral mode since it's treated as subsequent boot)
let script = "/bin/sh -c 'cat /sys/firmware/qemu_fw_cfg/by_name/opt/com.coreos/config/raw 2>/dev/null && echo FW_CFG_FOUND || echo FW_CFG_NOT_FOUND'";

let stdout = cmd!(
sh,
"{bck} ephemeral run --rm --label {label} --ignition {config_path} --execute {script} {FCOS_IMAGE}"
)
.read()?;

assert!(
stdout.contains("FW_CFG_FOUND"),
"fw_cfg config should be readable"
);
assert!(
stdout.contains("ignition"),
"Ignition config JSON should be accessible via fw_cfg, got: {}",
stdout
);
assert!(
!stdout.contains("FW_CFG_NOT_FOUND"),
"fw_cfg path should exist, got: {}",
stdout
);

Ok(())
}
integration_test!(test_run_ephemeral_ignition_fw_cfg_accessible);

/// Test that Ignition config validation rejects non-existent files
fn test_run_ephemeral_ignition_invalid_path() -> Result<()> {
let sh = shell()?;
let bck = get_bck_command()?;
let label = INTEGRATION_TEST_LABEL;

// Pull FCOS image first
cmd!(sh, "podman pull -q {FCOS_IMAGE}").run()?;

let nonexistent_path = "/tmp/nonexistent-ignition-config-12345.ign";

let output = cmd!(
sh,
"{bck} ephemeral run --rm --label {label} --ignition {nonexistent_path} --karg systemd.unit=poweroff.target {FCOS_IMAGE}"
)
.ignore_status()
.output()?;

assert!(
!output.status.success(),
"Should fail with non-existent Ignition config file"
);

let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("not found") || stderr.contains("does not exist"),
"Error should mention missing file: {}",
stderr
);

Ok(())
}
integration_test!(test_run_ephemeral_ignition_invalid_path);

/// Test that Ignition config validation rejects directories
fn test_run_ephemeral_ignition_directory_rejected() -> Result<()> {
let sh = shell()?;
let bck = get_bck_command()?;
let label = INTEGRATION_TEST_LABEL;

// Pull FCOS image first
cmd!(sh, "podman pull -q {FCOS_IMAGE}").run()?;

let temp_dir = TempDir::new()?;
let dir_path = Utf8Path::from_path(temp_dir.path()).expect("temp dir is not utf8");

let output = cmd!(
sh,
"{bck} ephemeral run --rm --label {label} --ignition {dir_path} --karg systemd.unit=poweroff.target {FCOS_IMAGE}"
)
.ignore_status()
.output()?;

assert!(
!output.status.success(),
"Should fail when Ignition config path is a directory"
);

let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("not a regular file") || stderr.contains("is a directory"),
"Error should mention that path is not a file: {}",
stderr
);

Ok(())
}
integration_test!(test_run_ephemeral_ignition_directory_rejected);

/// Test that Ignition is rejected for images that don't support it
fn test_run_ephemeral_ignition_unsupported_image() -> Result<()> {
let sh = shell()?;
let bck = get_bck_command()?;
let label = INTEGRATION_TEST_LABEL;

// Use standard bootc image that doesn't have Ignition support
let image = "quay.io/centos-bootc/centos-bootc:stream10";

let temp_dir = TempDir::new()?;
let config_path = Utf8Path::from_path(temp_dir.path())
.expect("temp dir is not utf8")
.join("config.ign");

let ignition_config = r#"{"ignition": {"version": "3.3.0"}}"#;
fs::write(&config_path, ignition_config)?;

let output = cmd!(
sh,
"{bck} ephemeral run --rm --label {label} --ignition {config_path} --karg systemd.unit=poweroff.target {image}"
)
.ignore_status()
.output()?;

assert!(
!output.status.success(),
"Should fail when using --ignition with non-Ignition image"
);

let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("does not support Ignition"),
"Error should mention missing Ignition support: {}",
stderr
);

Ok(())
}
integration_test!(test_run_ephemeral_ignition_unsupported_image);

/// Test that ignition.platform.id=qemu kernel argument is set when using Ignition
fn test_run_ephemeral_ignition_platform_id_karg() -> Result<()> {
let sh = shell()?;
let bck = get_bck_command()?;
let label = INTEGRATION_TEST_LABEL;

// Pull FCOS image first
cmd!(sh, "podman pull -q {FCOS_IMAGE}").run()?;

let temp_dir = TempDir::new()?;
let config_path = Utf8Path::from_path(temp_dir.path())
.expect("temp dir is not utf8")
.join("config.ign");

let ignition_config = r#"{"ignition": {"version": "3.3.0"}}"#;
fs::write(&config_path, ignition_config)?;

// Check /proc/cmdline for the Ignition platform ID kernel argument
let script = "/bin/sh -c 'cat /proc/cmdline'";

let stdout = cmd!(
sh,
"{bck} ephemeral run --rm --label {label} --ignition {config_path} --execute {script} {FCOS_IMAGE}"
)
.read()?;

assert!(
stdout.contains("ignition.platform.id=qemu"),
"Kernel command line should contain ignition.platform.id=qemu, got: {}",
stdout
);

Ok(())
}
integration_test!(test_run_ephemeral_ignition_platform_id_karg);
19 changes: 19 additions & 0 deletions crates/kit/src/qemu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ pub trait QemuConfigExt {
serial: String,
format: F,
) -> &mut Self;

/// Add a virtio-blk device with specified format and readonly flag using kit's Format type.
fn add_virtio_blk_device_with_format_ro<F: Into<DiskFormat>>(
&mut self,
disk_file: String,
serial: String,
format: F,
readonly: bool,
) -> &mut Self;
}

impl QemuConfigExt for QemuConfig {
Expand All @@ -50,4 +59,14 @@ impl QemuConfigExt for QemuConfig {
) -> &mut Self {
self.add_virtio_blk_device(disk_file, serial, format.into())
}

fn add_virtio_blk_device_with_format_ro<F: Into<DiskFormat>>(
&mut self,
disk_file: String,
serial: String,
format: F,
readonly: bool,
) -> &mut Self {
self.add_virtio_blk_device_ro(disk_file, serial, format.into(), readonly)
}
}
Loading
Loading