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
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
138 changes: 138 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,138 @@
//! 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 injection mechanism works
///
/// This test verifies that the Ignition config injection mechanism is working
/// by checking that the ignition.platform.id=qemu kernel argument is set when
/// --ignition is specified. This works across all architectures.
///
/// Note: We don't test actual Ignition application here because FCOS won't
/// apply Ignition configs in ephemeral mode (treats it as subsequent boot).
/// The config injection works correctly for custom bootc images with Ignition.
fn test_run_ephemeral_ignition_works() -> 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"}}"#;
fs::write(&config_path, ignition_config)?;

// Check that the platform.id kernel arg is present
let script = "/bin/sh -c 'grep -q ignition.platform.id=qemu /proc/cmdline && echo KARG_FOUND'";

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

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

Ok(())
}
integration_test!(test_run_ephemeral_ignition_works);

/// Test that Ignition config validation rejects nonexistent 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 temp = TempDir::new()?;
let nonexistent_path = Utf8Path::from_path(temp.path())
.expect("temp dir is not utf8")
.join("nonexistent-config.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 nonexistent Ignition config file"
);

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

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

/// 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);
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