Skip to content

Commit 7187771

Browse files
committed
Extract QEMU/virtiofsd logic into bcvk-qemu crate
Right now we have this unfortunate duality in that for `ephemeral` we directly spawn qemu, but obviously the `libvirt` verb defers all that to libvirt. There are other use cases that effectively want to directly control qemu, and while there are certainly *other* Rust libraries wrapping qemu, ours is okay. This makes our internals a crate that could be used as a `git` dependency (no intention to publish to crates.io). The kit crate now re-exports from bcvk-qemu to maintain backward compatibility. A QemuConfigExt trait bridges kit's Format type with bcvk-qemu's DiskFormat. Tested by patching bootc PR #2018 to use bcvk-qemu via [patch.crates-io], which builds successfully and uses spawn_virtiofsd_async() for the anaconda test infrastructure. Assisted-by: OpenCode (claude-opus-4-5-20250114) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent 03d5164 commit 7187771

10 files changed

Lines changed: 1487 additions & 1256 deletions

File tree

Cargo.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bcvk-qemu/Cargo.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "bcvk-qemu"
3+
version = "0.1.0"
4+
edition = "2021"
5+
description = "QEMU VM management with virtiofs support"
6+
license = "MIT OR Apache-2.0"
7+
publish = false
8+
9+
[dependencies]
10+
camino = "1.1"
11+
cap-std-ext = { git = "https://github.com/coreos/cap-std-ext", rev = "cfdb25d51ffc697e70aa0d8d3cefe9ec2133bd0a" }
12+
color-eyre = { workspace = true }
13+
data-encoding = "2.9"
14+
libc = "0.2"
15+
nix = { version = "0.29", features = ["socket"] }
16+
rustix = { version = "1", features = ["pipe", "process"] }
17+
tokio = { version = "1", features = ["fs", "process", "time", "macros"] }
18+
tracing = { workspace = true }
19+
vsock = "=0.5.1"
20+
21+
[lints]
22+
workspace = true
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
//! Systemd credential injection for QEMU VMs.
2+
//!
3+
//! Provides functions for injecting configuration into VMs via systemd credentials
4+
//! using SMBIOS firmware variables (preferred) or kernel command-line arguments.
5+
//! Supports SSH keys, mount units, environment configuration, and AF_VSOCK setup.
6+
7+
use color_eyre::Result;
8+
9+
/// Convert a guest mount path to a systemd unit name.
10+
///
11+
/// Systemd requires mount unit names to match the mount path with:
12+
/// - Leading slash removed
13+
/// - All slashes replaced with dashes
14+
/// - All dashes in path components escaped as `\x2d`
15+
/// - .mount suffix added
16+
///
17+
/// # Examples
18+
///
19+
/// - `/mnt/data` -> `mnt-data.mount`
20+
/// - `/var/lib/data` -> `var-lib-data.mount`
21+
/// - `/data` -> `data.mount`
22+
/// - `/mnt/test-rw` -> `mnt-test\x2drw.mount`
23+
pub fn guest_path_to_unit_name(guest_path: &str) -> String {
24+
let path = guest_path.strip_prefix('/').unwrap_or(guest_path);
25+
26+
// Escape dashes in path components, then replace slashes with dashes
27+
let escaped = path
28+
.split('/')
29+
.map(|component| component.replace('-', "\\x2d"))
30+
.collect::<Vec<_>>()
31+
.join("-");
32+
33+
format!("{}.mount", escaped)
34+
}
35+
36+
/// Generate a systemd mount unit for virtiofs.
37+
///
38+
/// Creates a systemd mount unit that mounts a virtiofs filesystem at the specified
39+
/// guest path. The unit is configured to:
40+
/// - Mount type: virtiofs
41+
/// - Options: Include readonly flag if specified, plus SELinux context for RO mounts
42+
/// - Before=remote-fs.target to integrate with standard systemd mount ordering
43+
///
44+
/// We use remote-fs.target rather than local-fs.target because virtiofs is
45+
/// conceptually similar to a "remote" filesystem - it requires virtio transport
46+
/// infrastructure to be available, similar to how NFS requires network.
47+
///
48+
/// Returns the complete unit file content as a string.
49+
pub fn generate_virtiofs_mount_unit(
50+
virtiofs_tag: &str,
51+
guest_path: &str,
52+
readonly: bool,
53+
) -> String {
54+
let options = if readonly {
55+
// Default readonly mounts to usr_t - this helps avoid SELinux
56+
// issues when accessing them as container storage for example.
57+
"ro,context=system_u:object_r:usr_t:s0"
58+
} else {
59+
"rw"
60+
};
61+
62+
format!(
63+
"[Unit]\n\
64+
Description=Mount virtiofs tag {tag} at {path}\n\
65+
ConditionPathExists=!/etc/initrd-release\n\
66+
Before=remote-fs.target\n\
67+
\n\
68+
[Mount]\n\
69+
What={tag}\n\
70+
Where={path}\n\
71+
Type=virtiofs\n\
72+
Options={options}\n",
73+
tag = virtiofs_tag,
74+
path = guest_path,
75+
options = options
76+
)
77+
}
78+
79+
/// Generate SMBIOS credentials for a systemd mount unit.
80+
///
81+
/// Creates systemd credentials for:
82+
/// 1. The mount unit itself (via systemd.extra-unit)
83+
/// 2. A dropin for local-fs.target that wants this mount unit
84+
///
85+
/// Returns a vector of SMBIOS credential strings.
86+
pub fn smbios_creds_for_mount_unit(
87+
virtiofs_tag: &str,
88+
guest_path: &str,
89+
readonly: bool,
90+
) -> Result<Vec<String>> {
91+
let unit_name = guest_path_to_unit_name(guest_path);
92+
let mount_unit_content = generate_virtiofs_mount_unit(virtiofs_tag, guest_path, readonly);
93+
let encoded_mount = data_encoding::BASE64.encode(mount_unit_content.as_bytes());
94+
95+
let mount_cred =
96+
format!("io.systemd.credential.binary:systemd.extra-unit.{unit_name}={encoded_mount}");
97+
98+
// Create a dropin for local-fs.target that wants this mount
99+
let dropin_content = format!(
100+
"[Unit]\n\
101+
Wants={unit_name}\n"
102+
);
103+
let encoded_dropin = data_encoding::BASE64.encode(dropin_content.as_bytes());
104+
let dropin_cred = format!(
105+
"io.systemd.credential.binary:systemd.unit-dropin.local-fs.target~bcvk-mounts={encoded_dropin}"
106+
);
107+
108+
Ok(vec![mount_cred, dropin_cred])
109+
}
110+
111+
/// Generate SMBIOS credential string for AF_VSOCK systemd notification socket.
112+
///
113+
/// Creates a systemd credential that configures systemd to send notifications
114+
/// via AF_VSOCK instead of the default Unix socket. This enables host-guest
115+
/// communication for debugging VM boot sequences.
116+
///
117+
/// Returns a string for use with `qemu -smbios type=11,value="..."`
118+
pub fn smbios_cred_for_vsock_notify(host_cid: u32, port: u32) -> String {
119+
format!(
120+
"io.systemd.credential:vmm.notify_socket=vsock-stream:{}:{}",
121+
host_cid, port
122+
)
123+
}
124+
125+
/// Generate SMBIOS credentials for STORAGE_OPTS configuration.
126+
///
127+
/// Creates a systemd unit that conditionally appends STORAGE_OPTS to /etc/environment
128+
/// (for PAM sessions including SSH), plus a dropin to ensure it runs.
129+
///
130+
/// Returns a vector with:
131+
/// 1. The unit itself (systemd.extra-unit)
132+
/// 2. A dropin for sysinit.target to pull in the unit
133+
pub fn smbios_creds_for_storage_opts() -> Result<Vec<String>> {
134+
// Create systemd unit that conditionally appends to /etc/environment
135+
let unit_content = r#"[Unit]
136+
Description=Setup STORAGE_OPTS for bcvk
137+
DefaultDependencies=no
138+
Before=systemd-user-sessions.service
139+
140+
[Service]
141+
Type=oneshot
142+
ExecStart=/bin/sh -c 'grep -q STORAGE_OPTS /etc/environment || echo STORAGE_OPTS=additionalimagestore=/run/host-container-storage >> /etc/environment'
143+
RemainAfterExit=yes
144+
"#;
145+
let encoded_unit = data_encoding::BASE64.encode(unit_content.as_bytes());
146+
let unit_cred = format!(
147+
"io.systemd.credential.binary:systemd.extra-unit.bcvk-storage-opts.service={encoded_unit}"
148+
);
149+
150+
// Create dropin for sysinit.target to pull in our unit
151+
let dropin_content = "[Unit]\nWants=bcvk-storage-opts.service\n";
152+
let encoded_dropin = data_encoding::BASE64.encode(dropin_content.as_bytes());
153+
let dropin_cred = format!(
154+
"io.systemd.credential.binary:systemd.unit-dropin.sysinit.target~bcvk-storage={encoded_dropin}"
155+
);
156+
157+
Ok(vec![unit_cred, dropin_cred])
158+
}
159+
160+
/// Generate tmpfiles.d lines for STORAGE_OPTS in systemd contexts.
161+
///
162+
/// Configures STORAGE_OPTS for:
163+
/// - /etc/environment.d/: systemd user manager and user services
164+
/// - /etc/systemd/system.conf.d/: system-level systemd services
165+
pub fn storage_opts_tmpfiles_d_lines() -> String {
166+
concat!(
167+
"f /etc/environment.d/90-bcvk-storage.conf 0644 root root - STORAGE_OPTS=additionalimagestore=/run/host-container-storage\n",
168+
"d /etc/systemd/system.conf.d 0755 root root -\n",
169+
"f /etc/systemd/system.conf.d/90-bcvk-storage.conf 0644 root root - [Manager]\\nDefaultEnvironment=STORAGE_OPTS=additionalimagestore=/run/host-container-storage\n"
170+
).to_string()
171+
}
172+
173+
/// Generate SMBIOS credential string for root SSH access.
174+
///
175+
/// Creates a systemd credential for QEMU's SMBIOS interface. Preferred method
176+
/// as it keeps credentials out of kernel command line and boot logs.
177+
///
178+
/// Returns a string for use with `qemu -smbios type=11,value="..."`
179+
pub fn smbios_cred_for_root_ssh(pubkey: &str) -> Result<String> {
180+
let k = key_to_root_tmpfiles_d(pubkey);
181+
let encoded = data_encoding::BASE64.encode(k.as_bytes());
182+
let r = format!("io.systemd.credential.binary:tmpfiles.extra={encoded}");
183+
Ok(r)
184+
}
185+
186+
/// Generate kernel command-line argument for root SSH access.
187+
///
188+
/// Creates a systemd credential for kernel command-line delivery. Less secure
189+
/// than SMBIOS method as credentials are visible in /proc/cmdline and boot logs.
190+
///
191+
/// Returns a string for use in kernel boot parameters.
192+
#[allow(dead_code)]
193+
pub fn karg_for_root_ssh(pubkey: &str) -> Result<String> {
194+
let k = key_to_root_tmpfiles_d(pubkey);
195+
let encoded = data_encoding::BASE64.encode(k.as_bytes());
196+
let r = format!("systemd.set_credential_binary=tmpfiles.extra:{encoded}");
197+
Ok(r)
198+
}
199+
200+
/// Convert SSH public key to systemd tmpfiles.d configuration.
201+
///
202+
/// Generates configuration to create `/root/.ssh` directory (0750) and
203+
/// `/root/.ssh/authorized_keys` file (700) with the Base64-encoded SSH key.
204+
/// Uses `f+~` to append to existing authorized_keys files.
205+
pub fn key_to_root_tmpfiles_d(pubkey: &str) -> String {
206+
let buf = data_encoding::BASE64.encode(pubkey.as_bytes());
207+
format!("d /root/.ssh 0750 - - -\nf+~ /root/.ssh/authorized_keys 700 - - - {buf}\n")
208+
}
209+
210+
#[cfg(test)]
211+
mod tests {
212+
use data_encoding::BASE64;
213+
214+
use super::*;
215+
216+
/// Test SSH public key for validation (truncated for brevity)
217+
const STUBKEY: &str = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC...";
218+
219+
/// Test tmpfiles.d configuration generation
220+
#[test]
221+
fn test_key_to_root_tmpfiles_d() {
222+
let expected = "d /root/.ssh 0750 - - -\nf+~ /root/.ssh/authorized_keys 700 - - - c3NoLXJzYSBBQUFBQjNOemFDMXljMkVBQUFBREFRQUJBQUFCQVFDLi4u\n";
223+
assert_eq!(key_to_root_tmpfiles_d(STUBKEY), expected);
224+
}
225+
226+
/// Test SMBIOS credential generation and format validation
227+
#[test]
228+
fn test_credential_for_root_ssh() {
229+
let b64_tmpfiles = BASE64.encode(key_to_root_tmpfiles_d(STUBKEY).as_bytes());
230+
let expected = format!("io.systemd.credential.binary:tmpfiles.extra={b64_tmpfiles}");
231+
232+
// Verify credential format by reverse parsing
233+
let v = expected
234+
.strip_prefix("io.systemd.credential.binary:")
235+
.unwrap();
236+
let v = v.strip_prefix("tmpfiles.extra=").unwrap();
237+
let v = String::from_utf8(BASE64.decode(v.as_bytes()).unwrap()).unwrap();
238+
assert_eq!(v, "d /root/.ssh 0750 - - -\nf+~ /root/.ssh/authorized_keys 700 - - - c3NoLXJzYSBBQUFBQjNOemFDMXljMkVBQUFBREFRQUJBQUFCQVFDLi4u\n");
239+
240+
// Test the actual function output
241+
assert_eq!(smbios_cred_for_root_ssh(STUBKEY).unwrap(), expected);
242+
}
243+
244+
#[test]
245+
fn test_guest_path_to_unit_name() {
246+
assert_eq!(guest_path_to_unit_name("/mnt/data"), "mnt-data.mount");
247+
assert_eq!(
248+
guest_path_to_unit_name("/var/lib/data"),
249+
"var-lib-data.mount"
250+
);
251+
assert_eq!(guest_path_to_unit_name("/data"), "data.mount");
252+
assert_eq!(
253+
guest_path_to_unit_name("/mnt/test-rw"),
254+
"mnt-test\\x2drw.mount"
255+
);
256+
}
257+
}

crates/bcvk-qemu/src/lib.rs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//! QEMU virtualization library with virtiofs support.
2+
//!
3+
//! This crate provides a Rust interface for launching and managing QEMU virtual
4+
//! machines with VirtIO devices, particularly virtiofs filesystem mounts.
5+
//!
6+
//! # Features
7+
//!
8+
//! - **QEMU VM Management**: Launch VMs with direct kernel boot, virtio devices,
9+
//! and automatic resource cleanup
10+
//! - **VirtioFS Mounts**: Spawn and manage virtiofsd processes for sharing host
11+
//! directories with the guest
12+
//! - **SMBIOS Credentials**: Inject systemd credentials via QEMU SMBIOS interface
13+
//! for passwordless authentication and configuration
14+
//! - **VirtIO Serial**: Configure virtio-serial devices for guest-to-host
15+
//! communication (e.g., log streaming)
16+
//!
17+
//! # Example
18+
//!
19+
//! ```no_run
20+
//! use bcvk_qemu::{QemuConfig, VirtiofsConfig};
21+
//!
22+
//! # async fn example() -> color_eyre::Result<()> {
23+
//! // Configure a VM with direct kernel boot
24+
//! let mut config = QemuConfig::new_direct_boot(
25+
//! 2048, // memory_mb
26+
//! 2, // vcpus
27+
//! "/path/to/kernel".to_string(),
28+
//! "/path/to/initramfs".to_string(),
29+
//! "/tmp/virtiofs.sock".into(),
30+
//! );
31+
//!
32+
//! // Add kernel command line arguments
33+
//! config.set_kernel_cmdline(vec![
34+
//! "console=ttyS0".to_string(),
35+
//! "root=rootfs".to_string(),
36+
//! "rw".to_string(),
37+
//! ]);
38+
//!
39+
//! // Enable console output
40+
//! config.set_console(true);
41+
//! # Ok(())
42+
//! # }
43+
//! ```
44+
45+
mod credentials;
46+
mod qemu;
47+
mod virtiofsd;
48+
49+
pub use credentials::{
50+
generate_virtiofs_mount_unit, guest_path_to_unit_name, key_to_root_tmpfiles_d,
51+
smbios_cred_for_root_ssh, smbios_cred_for_vsock_notify, smbios_creds_for_mount_unit,
52+
smbios_creds_for_storage_opts, storage_opts_tmpfiles_d_lines,
53+
};
54+
55+
pub use qemu::{
56+
BootMode, DiskFormat, DisplayMode, NetworkMode, QemuConfig, ResourceLimits, RunningQemu,
57+
VirtioBlkDevice, VirtioSerialOut, VirtiofsMount, VHOST_VSOCK,
58+
};
59+
60+
pub use virtiofsd::{spawn_virtiofsd_async, validate_virtiofsd_config, VirtiofsConfig};

0 commit comments

Comments
 (0)