Skip to content

Commit 1ac8a32

Browse files
committed
ephemeral: Add Ignition config injection support
Add support for injecting Ignition configuration files into ephemeral VMs via QEMU's fw_cfg mechanism (x86_64/aarch64) and virtio-blk (s390x/ppc64le). This enables first-boot provisioning for bootc-based systems that use Ignition. Assisted-by: Claude Code (Sonnet 4.5) Signed-off-by: gursewak1997 <gursmangat@gmail.com>
1 parent 5c42e91 commit 1ac8a32

File tree

9 files changed

+425
-2
lines changed

9 files changed

+425
-2
lines changed

crates/bcvk-qemu/src/qemu.rs

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ pub struct VirtioBlkDevice {
7878
pub serial: String,
7979
/// Disk image format.
8080
pub format: DiskFormat,
81+
/// Mount as read-only.
82+
pub readonly: bool,
8183
}
8284

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

226228
vhost_fd: Option<File>,
229+
230+
/// fw_cfg entries for passing config files to the guest
231+
fw_cfg_entries: Vec<(String, Utf8PathBuf)>,
227232
}
228233

229234
impl QemuConfig {
@@ -365,11 +370,23 @@ impl QemuConfig {
365370
disk_file: String,
366371
serial: String,
367372
format: DiskFormat,
373+
) -> &mut Self {
374+
self.add_virtio_blk_device_ro(disk_file, serial, format, false)
375+
}
376+
377+
/// Add a virtio-blk device with specified format and readonly flag.
378+
pub fn add_virtio_blk_device_ro(
379+
&mut self,
380+
disk_file: String,
381+
serial: String,
382+
format: DiskFormat,
383+
readonly: bool,
368384
) -> &mut Self {
369385
self.virtio_blk_devices.push(VirtioBlkDevice {
370386
disk_file,
371387
serial,
372388
format,
389+
readonly,
373390
});
374391
self
375392
}
@@ -440,6 +457,13 @@ impl QemuConfig {
440457
};
441458
self
442459
}
460+
461+
/// Add a fw_cfg entry to pass a file to the guest.
462+
/// The file will be accessible in the guest via the fw_cfg interface.
463+
pub fn add_fw_cfg(&mut self, name: String, file_path: Utf8PathBuf) -> &mut Self {
464+
self.fw_cfg_entries.push((name, file_path));
465+
self
466+
}
443467
}
444468

445469
/// Allocate a unique VSOCK CID.
@@ -560,13 +584,19 @@ fn spawn(
560584
// Add virtio-blk block devices
561585
for (idx, blk_device) in config.virtio_blk_devices.iter().enumerate() {
562586
let drive_id = format!("drive{}", idx);
587+
let readonly_flag = if blk_device.readonly {
588+
",readonly=on"
589+
} else {
590+
""
591+
};
563592
cmd.args([
564593
"-drive",
565594
&format!(
566-
"file={},format={},if=none,id={}",
595+
"file={},format={},if=none,id={}{}",
567596
blk_device.disk_file,
568597
blk_device.format.as_str(),
569-
drive_id
598+
drive_id,
599+
readonly_flag
570600
),
571601
"-device",
572602
&format!(
@@ -723,6 +753,11 @@ fn spawn(
723753
cmd.args(["-smbios", &format!("type=11,value={}", credential)]);
724754
}
725755

756+
// Add fw_cfg entries
757+
for (name, file_path) in &config.fw_cfg_entries {
758+
cmd.args(["-fw_cfg", &format!("name={},file={}", name, file_path)]);
759+
}
760+
726761
// Configure stdio based on display mode
727762
match &config.display_mode {
728763
DisplayMode::Console => {
@@ -993,4 +1028,24 @@ mod tests {
9931028
assert_eq!(DiskFormat::Raw.as_str(), "raw");
9941029
assert_eq!(DiskFormat::Qcow2.as_str(), "qcow2");
9951030
}
1031+
1032+
#[test]
1033+
fn test_fw_cfg_entry() {
1034+
let mut config = QemuConfig::new_direct_boot(
1035+
1024,
1036+
1,
1037+
"/test/kernel".to_string(),
1038+
"/test/initramfs".to_string(),
1039+
"/test/socket".into(),
1040+
);
1041+
config.add_fw_cfg(
1042+
"opt/com.coreos/config".to_string(),
1043+
"/test/ignition.json".into(),
1044+
);
1045+
1046+
// Test that the fw_cfg entry is created correctly
1047+
assert_eq!(config.fw_cfg_entries.len(), 1);
1048+
assert_eq!(config.fw_cfg_entries[0].0, "opt/com.coreos/config");
1049+
assert_eq!(config.fw_cfg_entries[0].1.as_str(), "/test/ignition.json");
1050+
}
9961051
}

crates/integration-tests/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ mod tests {
2121
pub mod libvirt_verb;
2222
pub mod mount_feature;
2323
pub mod run_ephemeral;
24+
pub mod run_ephemeral_ignition;
2425
pub mod run_ephemeral_ssh;
2526
pub mod to_disk;
2627
pub mod varlink;
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
//! Integration tests for Ignition config injection
2+
3+
use color_eyre::Result;
4+
use integration_tests::integration_test;
5+
use xshell::cmd;
6+
7+
use std::fs;
8+
use tempfile::TempDir;
9+
10+
use camino::Utf8Path;
11+
12+
use crate::{get_bck_command, shell, INTEGRATION_TEST_LABEL};
13+
14+
/// Fedora CoreOS image that supports Ignition
15+
const FCOS_IMAGE: &str = "quay.io/fedora/fedora-coreos:stable";
16+
17+
/// Test that Ignition config injection mechanism works
18+
///
19+
/// This test verifies that the Ignition config injection mechanism is working
20+
/// by checking that the ignition.platform.id=qemu kernel argument is set when
21+
/// --ignition is specified. This works across all architectures.
22+
///
23+
/// Note: We don't test actual Ignition application here because FCOS won't
24+
/// apply Ignition configs in ephemeral mode (treats it as subsequent boot).
25+
/// The config injection works correctly for custom bootc images with Ignition.
26+
fn test_run_ephemeral_ignition_works() -> Result<()> {
27+
let sh = shell()?;
28+
let bck = get_bck_command()?;
29+
let label = INTEGRATION_TEST_LABEL;
30+
31+
// Pull FCOS image first
32+
cmd!(sh, "podman pull -q {FCOS_IMAGE}").run()?;
33+
34+
// Create a temporary Ignition config
35+
let temp_dir = TempDir::new()?;
36+
let config_path = Utf8Path::from_path(temp_dir.path())
37+
.expect("temp dir is not utf8")
38+
.join("config.ign");
39+
40+
// Minimal valid Ignition config (v3.3.0 for FCOS)
41+
let ignition_config = r#"{"ignition": {"version": "3.3.0"}}"#;
42+
fs::write(&config_path, ignition_config)?;
43+
44+
// Check that the platform.id kernel arg is present
45+
let script = "/bin/sh -c 'grep -q ignition.platform.id=qemu /proc/cmdline && echo KARG_FOUND'";
46+
47+
let stdout = cmd!(
48+
sh,
49+
"{bck} ephemeral run --rm --label {label} --ignition {config_path} --execute {script} {FCOS_IMAGE}"
50+
)
51+
.read()?;
52+
53+
assert!(
54+
stdout.contains("KARG_FOUND"),
55+
"Kernel command line should contain ignition.platform.id=qemu, got: {}",
56+
stdout
57+
);
58+
59+
Ok(())
60+
}
61+
integration_test!(test_run_ephemeral_ignition_works);
62+
63+
/// Test that Ignition config validation rejects nonexistent files
64+
fn test_run_ephemeral_ignition_invalid_path() -> Result<()> {
65+
let sh = shell()?;
66+
let bck = get_bck_command()?;
67+
let label = INTEGRATION_TEST_LABEL;
68+
69+
// Pull FCOS image first
70+
cmd!(sh, "podman pull -q {FCOS_IMAGE}").run()?;
71+
72+
let temp = TempDir::new()?;
73+
let nonexistent_path = Utf8Path::from_path(temp.path())
74+
.expect("temp dir is not utf8")
75+
.join("nonexistent-config.ign");
76+
77+
let output = cmd!(
78+
sh,
79+
"{bck} ephemeral run --rm --label {label} --ignition {nonexistent_path} --karg systemd.unit=poweroff.target {FCOS_IMAGE}"
80+
)
81+
.ignore_status()
82+
.output()?;
83+
84+
assert!(
85+
!output.status.success(),
86+
"Should fail with nonexistent Ignition config file"
87+
);
88+
89+
let stderr = String::from_utf8_lossy(&output.stderr);
90+
assert!(
91+
stderr.contains("not found"),
92+
"Error should mention missing file: {}",
93+
stderr
94+
);
95+
96+
Ok(())
97+
}
98+
integration_test!(test_run_ephemeral_ignition_invalid_path);
99+
100+
/// Test that Ignition is rejected for images that don't support it
101+
fn test_run_ephemeral_ignition_unsupported_image() -> Result<()> {
102+
let sh = shell()?;
103+
let bck = get_bck_command()?;
104+
let label = INTEGRATION_TEST_LABEL;
105+
106+
// Use standard bootc image that doesn't have Ignition support
107+
let image = "quay.io/centos-bootc/centos-bootc:stream10";
108+
109+
let temp_dir = TempDir::new()?;
110+
let config_path = Utf8Path::from_path(temp_dir.path())
111+
.expect("temp dir is not utf8")
112+
.join("config.ign");
113+
114+
let ignition_config = r#"{"ignition": {"version": "3.3.0"}}"#;
115+
fs::write(&config_path, ignition_config)?;
116+
117+
let output = cmd!(
118+
sh,
119+
"{bck} ephemeral run --rm --label {label} --ignition {config_path} --karg systemd.unit=poweroff.target {image}"
120+
)
121+
.ignore_status()
122+
.output()?;
123+
124+
assert!(
125+
!output.status.success(),
126+
"Should fail when using --ignition with non-Ignition image"
127+
);
128+
129+
let stderr = String::from_utf8_lossy(&output.stderr);
130+
assert!(
131+
stderr.contains("does not support Ignition"),
132+
"Error should mention missing Ignition support: {}",
133+
stderr
134+
);
135+
136+
Ok(())
137+
}
138+
integration_test!(test_run_ephemeral_ignition_unsupported_image);

crates/kit/src/qemu.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ pub trait QemuConfigExt {
3939
serial: String,
4040
format: F,
4141
) -> &mut Self;
42+
43+
/// Add a virtio-blk device with specified format and readonly flag using kit's Format type.
44+
fn add_virtio_blk_device_with_format_ro<F: Into<DiskFormat>>(
45+
&mut self,
46+
disk_file: String,
47+
serial: String,
48+
format: F,
49+
readonly: bool,
50+
) -> &mut Self;
4251
}
4352

4453
impl QemuConfigExt for QemuConfig {
@@ -50,4 +59,14 @@ impl QemuConfigExt for QemuConfig {
5059
) -> &mut Self {
5160
self.add_virtio_blk_device(disk_file, serial, format.into())
5261
}
62+
63+
fn add_virtio_blk_device_with_format_ro<F: Into<DiskFormat>>(
64+
&mut self,
65+
disk_file: String,
66+
serial: String,
67+
format: F,
68+
readonly: bool,
69+
) -> &mut Self {
70+
self.add_virtio_blk_device_ro(disk_file, serial, format.into(), readonly)
71+
}
5372
}

0 commit comments

Comments
 (0)