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
3 changes: 3 additions & 0 deletions contrib/packaging/bcvk.spec
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ Requires: podman
Requires: qemu-img
Requires: qemu-kvm
Requires: virtiofsd
# Extraction and initramfs tools are needed for some ephemeral boot images.
Recommends: binutils
Recommends: kernel-tools
# libvirt-client is optional but recommended for 'bcvk libvirt' commands
Recommends: libvirt-client

Expand Down
14 changes: 0 additions & 14 deletions crates/bcvk-qemu/src/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,20 +183,6 @@ pub fn smbios_cred_for_root_ssh(pubkey: &str) -> Result<String> {
Ok(r)
}

/// Generate kernel command-line argument for root SSH access.
///
/// Creates a systemd credential for kernel command-line delivery. Less secure
/// than SMBIOS method as credentials are visible in /proc/cmdline and boot logs.
///
/// Returns a string for use in kernel boot parameters.
#[allow(dead_code)]
pub fn karg_for_root_ssh(pubkey: &str) -> Result<String> {
let k = key_to_root_tmpfiles_d(pubkey);
let encoded = data_encoding::BASE64.encode(k.as_bytes());
let r = format!("systemd.set_credential_binary=tmpfiles.extra:{encoded}");
Ok(r)
}

/// Convert SSH public key to systemd tmpfiles.d configuration.
///
/// Generates configuration to create `/root/.ssh` directory (0750) and
Expand Down
4 changes: 2 additions & 2 deletions crates/bcvk-qemu/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ pub use credentials::{
};

pub use qemu::{
BootMode, DiskFormat, DisplayMode, MachineType, NetworkMode, QemuConfig, ResourceLimits,
RunningQemu, VirtioBlkDevice, VirtioSerialOut, VirtiofsMount, VHOST_VSOCK,
qemu_version, BootMode, DiskFormat, DisplayMode, MachineType, NetworkMode, QemuConfig,
ResourceLimits, RunningQemu, VirtioBlkDevice, VirtioSerialOut, VirtiofsMount, VHOST_VSOCK,
};

pub use virtiofsd::{spawn_virtiofsd_async, validate_virtiofsd_config, VirtiofsConfig};
115 changes: 97 additions & 18 deletions crates/bcvk-qemu/src/qemu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ use crate::VirtiofsConfig;
/// The device path for vsock allocation.
pub const VHOST_VSOCK: &str = "/dev/vhost-vsock";

const SYSTEMD_CREDENTIAL_FW_CFG_PREFIX: &str = "opt/io.systemd.credentials/";

/// VirtIO-FS mount point configuration.
#[derive(Debug, Clone)]
pub struct VirtiofsMount {
Expand Down Expand Up @@ -464,6 +466,19 @@ impl QemuConfig {
self.fw_cfg_entries.push((name, file_path));
self
}

/// Pass a systemd system credential through QEMU's fw_cfg interface.
/// Only the file path, not its contents, appears in QEMU's arguments.
pub fn add_systemd_credential_file(
&mut self,
credential_name: &str,
file_path: Utf8PathBuf,
) -> &mut Self {
self.add_fw_cfg(
format!("{SYSTEMD_CREDENTIAL_FW_CFG_PREFIX}{credential_name}"),
file_path,
)
}
}

/// Allocate a unique VSOCK CID.
Expand Down Expand Up @@ -505,6 +520,63 @@ fn allocate_vsock_cid(vhost_fd: File) -> Result<(OwnedFd, u32)> {
Err(eyre!("Could not find available VSOCK CID (tried 3-10000)"))
}

/// Resolve the QEMU executable used to launch the VM.
fn qemu_binary() -> Result<String> {
std::env::var("QEMU_BIN")
.ok()
.map(Ok)
.unwrap_or_else(|| -> Result<_> {
// RHEL only supports non-emulated, and qemu is an implementation detail
// of higher level virt.
let libexec_qemu = Utf8Path::new("/usr/libexec/qemu-kvm");
if libexec_qemu.try_exists()? {
Ok(libexec_qemu.to_string())
} else {
let arch = std::env::consts::ARCH;
Ok(format!("qemu-system-{arch}"))
}
})
.context("Checking for qemu")
}

/// Parse the major, minor, and patch version from QEMU's `--version` output.
fn parse_qemu_version(output: &str) -> Option<(u32, u32, u32)> {
let version = output
.lines()
.next()?
.strip_prefix("QEMU emulator version ")?
.split_whitespace()
.next()?;
let numeric = version.split_once('-').map_or(version, |(base, _)| base);
let mut parts = numeric.split('.');
let major = parts.next()?.parse::<u32>().ok()?;
let minor = parts.next()?.parse::<u32>().ok()?;
let patch = parts.next()?.parse::<u32>().ok()?;
if parts.next().is_some() {
return None;
}
Some((major, minor, patch))
}

/// Query the version of the same QEMU executable used to launch VMs.
pub fn qemu_version() -> Result<Option<(u32, u32, u32)>> {
let binary = qemu_binary()?;
let output = Command::new(&binary)
.arg("--version")
.output()
.with_context(|| format!("Checking version of {binary}"))?;
if !output.status.success() {
warn!("Could not get {binary} version");
return Ok(None);
}
let version_output = String::from_utf8_lossy(&output.stdout);
let version = parse_qemu_version(&version_output);
if version.is_none() {
warn!("Could not parse {binary} version");
}
Ok(version)
}

/// Spawn QEMU VM process with given configuration and optional extra credential.
/// Uses KVM acceleration, memory-backend-memfd for VirtIO-FS compatibility.
fn spawn(
Expand All @@ -520,23 +592,7 @@ fn spawn(
config.memory_mb
);

let qemu = std::env::var("QEMU_BIN")
.ok()
.map(Ok)
.unwrap_or_else(|| -> Result<_> {
// RHEL only supports non-emulated, and qemu is an implementation detail
// of higher level virt.
let libexec_qemu = Utf8Path::new("/usr/libexec/qemu-kvm");
if libexec_qemu.try_exists()? {
Ok(libexec_qemu.to_string())
} else {
let arch = std::env::consts::ARCH;
Ok(format!("qemu-system-{arch}"))
}
})
.context("Checking for qemu")?;

let mut cmd = Command::new(qemu);
let mut cmd = Command::new(qemu_binary()?);
// SAFETY: This API is safe to call in a forked child.
#[allow(unsafe_code)]
unsafe {
Expand Down Expand Up @@ -992,6 +1048,23 @@ impl RunningQemu {
mod tests {
use super::*;

#[test]
fn test_parse_qemu_version() {
for (output, expected) in [
(
"QEMU emulator version 10.2.2 (qemu-10.2.2-1.fc44)",
Some((10, 2, 2)),
),
("QEMU emulator version 11.0.0", Some((11, 0, 0))),
("QEMU emulator version 11.1.0", Some((11, 1, 0))),
("QEMU emulator version 12.0.0", Some((12, 0, 0))),
("QEMU emulator version unknown", None),
("not a QEMU version", None),
] {
assert_eq!(parse_qemu_version(output), expected);
}
}

#[test]
fn test_virtio_serial_device_creation() {
let mut config = QemuConfig::new_direct_boot(
Expand Down Expand Up @@ -1045,10 +1118,16 @@ mod tests {
"opt/com.coreos/config".to_string(),
"/test/ignition.json".into(),
);
config.add_systemd_credential_file("tmpfiles.extra", "/test/ssh-credential".into());

// Test that the fw_cfg entry is created correctly
assert_eq!(config.fw_cfg_entries.len(), 1);
assert_eq!(config.fw_cfg_entries.len(), 2);
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");
assert_eq!(
config.fw_cfg_entries[1].0,
"opt/io.systemd.credentials/tmpfiles.extra"
);
assert_eq!(config.fw_cfg_entries[1].1.as_str(), "/test/ssh-credential");
}
}
4 changes: 2 additions & 2 deletions crates/kit/src/credentials.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
//!
//! This module re-exports credential types and functions from the bcvk-qemu crate.
//! Provides functions for injecting configuration into VMs via systemd credentials
//! using SMBIOS firmware variables (preferred) or kernel command-line arguments.
//! using SMBIOS firmware variables or QEMU fw_cfg.
//! Supports SSH keys, mount units, environment configuration, and AF_VSOCK setup.

// Re-export credential functions from bcvk-qemu that are used within kit
pub use bcvk_qemu::{
generate_virtiofs_mount_unit, guest_path_to_unit_name, key_to_root_tmpfiles_d,
smbios_cred_for_root_ssh, smbios_creds_for_storage_opts, storage_opts_tmpfiles_d_lines,
smbios_creds_for_storage_opts, storage_opts_tmpfiles_d_lines,
};
Loading
Loading