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
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
uses: Swatinem/rust-cache@v2

- name: Build
run: just check && just build
run: just validate && just build

- name: Run unit tests
run: just unit
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ unused_must_use = "forbid"
missing_docs = "deny"
missing_debug_implementations = "deny"
# Feel free to comment this one out locally during development of a patch.
unused_imports = "deny"
dead_code = "deny"

[workspace.lints.clippy]
Expand Down
7 changes: 3 additions & 4 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
build:
make

# Quick checks
check:
cargo t --workspace --no-run
cargo fmt --check
# Static checks
validate:
make validate

# Run unit tests (excludes integration tests)
unit *ARGS:
Expand Down
13 changes: 13 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ manpages: sync-cli-options $(MAN8_TARGETS)
sync-cli-options:
@cargo xtask sync-manpages >/dev/null 2>&1 || true

# This gates CI by default. Note that for clippy, we gate on
# only the clippy correctness and suspicious lints, plus a select
# set of default rustc warnings.
# We intentionally don't gate on this for local builds in cargo.toml
# because it impedes iteration speed.
CLIPPY_CONFIG = -A clippy::all -D clippy::correctness -D clippy::suspicious -D clippy::disallowed-methods -Dunused_imports -Ddead_code
validate:
cargo fmt -- --check -l
cargo test --no-run --workspace
cargo clippy -- $(CLIPPY_CONFIG)
env RUSTDOCFLAGS='-D warnings' cargo doc --lib
.PHONY: validate

install:
install -D -m 0755 -t $(DESTDIR)$(prefix)/bin target/release/bcvk
if [ -n "$(MAN8_TARGETS)" ]; then \
Expand Down
3 changes: 3 additions & 0 deletions crates/integration-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ uuid = { version = "1.18.1", features = ["v4"] }
camino = "1.1.12"
regex = "1"
linkme = "0.3.30"

[lints]
workspace = true
4 changes: 4 additions & 0 deletions crates/integration-tests/src/bin/cleanup.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
//! Cleanup utility for integration test resources
//!
//! This binary removes integration test containers and libvirt VMs that were created during testing.

use std::process::Command;

// Import shared constants from the library
Expand Down
3 changes: 3 additions & 0 deletions crates/integration-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ pub type TestFn = fn() -> color_eyre::Result<()>;
/// Metadata for a registered integration test
#[derive(Debug)]
pub struct IntegrationTest {
/// Name of the integration test
pub name: &'static str,
/// Test function to execute
pub f: TestFn,
}

impl IntegrationTest {
/// Create a new integration test with the given name and function
pub const fn new(name: &'static str, f: TestFn) -> Self {
Self { name, f }
}
Expand Down
2 changes: 2 additions & 0 deletions crates/integration-tests/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Integration tests for bcvk

use camino::Utf8Path;
use std::process::Output;

Expand Down
2 changes: 2 additions & 0 deletions crates/kit/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,5 @@ similar-asserts = "1.5"
# Implementation detail of man page generation.
docgen = ["clap_mangen"]

[lints]
workspace = true
4 changes: 3 additions & 1 deletion crates/kit/src/domain_list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,9 @@ impl DomainLister {
.unwrap_or_default();

// Extract memory and vcpu from domain XML
let memory_mb = dom.find("memory").and_then(|node| node.parse_memory_mb());
let memory_mb = dom
.find("memory")
.and_then(|node| crate::libvirt::parse_memory_mb(node));

let vcpus = dom
.find("vcpu")
Expand Down
1 change: 0 additions & 1 deletion crates/kit/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
//! bcvk library - exposes internal modules for testing

pub mod qemu_img;
pub mod utils;
Comment thread
cgwalters marked this conversation as resolved.
pub mod xml_utils;
2 changes: 1 addition & 1 deletion crates/kit/src/libvirt/domain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ impl DomainBuilder {
writer.end_element("interface")?;
}
network if network.starts_with("bridge=") => {
let bridge_name = &network[7..]; // Remove "bridge=" prefix
let bridge_name = network.strip_prefix("bridge=").unwrap();
writer.start_element("interface", &[("type", "bridge")])?;
writer.write_empty_element("source", &[("bridge", bridge_name)])?;
writer.write_empty_element("model", &[("type", "virtio")])?;
Expand Down
11 changes: 1 addition & 10 deletions crates/kit/src/libvirt/list_volumes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -344,8 +344,6 @@ impl LibvirtListVolumesOpts {
}
}

/// Extract value from XML element (simple string parsing)

/// Parse virsh size format (e.g., "5.00 GiB") to bytes
fn parse_virsh_size(size_str: &str) -> Option<u64> {
let parts: Vec<&str> = size_str.split_whitespace().collect();
Expand All @@ -356,14 +354,7 @@ fn parse_virsh_size(size_str: &str) -> Option<u64> {
let number: f64 = parts[0].parse().ok()?;
let unit = parts[1];

let multiplier = match unit {
"B" | "bytes" => 1,
"KiB" | "KB" => 1024,
"MiB" | "MB" => 1024 * 1024,
"GiB" | "GB" => 1024 * 1024 * 1024,
"TiB" | "TB" => 1024u64.pow(4),
_ => return None,
};
let multiplier = super::unit_to_bytes(unit)?;

Some((number * multiplier as f64) as u64)
}
Expand Down
120 changes: 120 additions & 0 deletions crates/kit/src/libvirt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,126 @@ impl LibvirtOptions {
}
}

/// Convert a unit string to bytes multiplier
/// Handles libvirt-style units distinguishing between decimal (KB, MB, GB - powers of 1000)
/// and binary (KiB, MiB, GiB - powers of 1024) units per libvirt specification
pub(crate) fn unit_to_bytes(unit: &str) -> Option<u128> {
match unit {
// Binary prefixes (powers of 1024)
"B" | "bytes" => Some(1),
"k" | "K" | "KiB" => Some(1024),
"M" | "MiB" => Some(1024u128.pow(2)),
"G" | "GiB" => Some(1024u128.pow(3)),
"T" | "TiB" => Some(1024u128.pow(4)),

// Decimal prefixes (powers of 1000)
"KB" => Some(1_000),
"MB" => Some(1_000u128.pow(2)),
"GB" => Some(1_000u128.pow(3)),
"TB" => Some(1_000u128.pow(4)),

_ => None,
}
}

/// Convert memory value with unit to megabytes (MiB)
/// Handles libvirt-style units distinguishing between decimal (KB, MB, GB - powers of 1000)
/// and binary (KiB, MiB, GiB - powers of 1024) units per libvirt specification
/// Returns None if the unit is unknown or if the result overflows u32
pub(crate) fn convert_memory_to_mb(value: u32, unit: &str) -> Option<u32> {
let value_u128 = value as u128;
let mib_u128 = 1024 * 1024;

// Convert to bytes first, then to MiB
let bytes = value_u128 * unit_to_bytes(unit)?;
let mb = bytes / mib_u128;

u32::try_from(mb).ok()
}

/// Convert memory value with unit to megabytes (MiB), returning u64
/// Handles libvirt-style units distinguishing between decimal (KB, MB, GB - powers of 1000)
/// and binary (KiB, MiB, GiB - powers of 1024) units per libvirt specification
/// Returns None if the unit is unknown or if the result overflows u64
#[allow(dead_code)]
pub(crate) fn convert_to_mb(value: u64, unit: &str) -> Option<u64> {
let value_u128 = value as u128;
let mib_u128 = 1024 * 1024;

// Convert to bytes first, then to MiB
let bytes = value_u128 * unit_to_bytes(unit)?;
let mb = bytes / mib_u128;

u64::try_from(mb).ok()
}

/// Parse memory value from a libvirt XML node with unit attribute
/// Returns the value in megabytes (MiB)
pub(crate) fn parse_memory_mb(node: &crate::xml_utils::XmlNode) -> Option<u32> {
let value = node.text_content().parse::<u32>().ok()?;
// Convert to MB based on unit attribute (default is KiB per libvirt spec)
let unit = node
.attributes
.get("unit")
.map(|s| s.as_str())
.unwrap_or("KiB");
convert_memory_to_mb(value, unit)
}
Comment thread
cgwalters marked this conversation as resolved.

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_convert_memory_to_mb() {
// Test binary units (powers of 1024)
assert_eq!(convert_memory_to_mb(4194304, "KiB"), Some(4096));
assert_eq!(convert_memory_to_mb(2097152, "KiB"), Some(2048));
assert_eq!(convert_memory_to_mb(2048, "MiB"), Some(2048));
assert_eq!(convert_memory_to_mb(4096, "MiB"), Some(4096));
assert_eq!(convert_memory_to_mb(4, "GiB"), Some(4096));
assert_eq!(convert_memory_to_mb(2, "GiB"), Some(2048));

// Test short forms (binary)
assert_eq!(convert_memory_to_mb(4, "G"), Some(4096));
assert_eq!(convert_memory_to_mb(2048, "M"), Some(2048));
assert_eq!(convert_memory_to_mb(2097152, "K"), Some(2048));

// Test decimal units (powers of 1000)
assert_eq!(convert_memory_to_mb(1048576, "KB"), Some(1000));
assert_eq!(convert_memory_to_mb(1024, "MB"), Some(976));
assert_eq!(convert_memory_to_mb(4, "GB"), Some(3814));

// Test unknown unit returns None
assert_eq!(convert_memory_to_mb(4194304, "unknown"), None);
}

#[test]
fn test_parse_memory_mb() {
use crate::xml_utils::parse_xml_dom;

// Test KiB (default unit)
let xml = r#"<memory>4194304</memory>"#;
let dom = parse_xml_dom(xml).unwrap();
assert_eq!(parse_memory_mb(&dom), Some(4096));

// Test MiB
let xml = r#"<memory unit='MiB'>2048</memory>"#;
let dom = parse_xml_dom(xml).unwrap();
assert_eq!(parse_memory_mb(&dom), Some(2048));

// Test GiB
let xml = r#"<memory unit='GiB'>4</memory>"#;
let dom = parse_xml_dom(xml).unwrap();
assert_eq!(parse_memory_mb(&dom), Some(4096));

// Test KB (decimal unit: 1000-based)
let xml = r#"<memory unit='KB'>1048576</memory>"#;
let dom = parse_xml_dom(xml).unwrap();
assert_eq!(parse_memory_mb(&dom), Some(1000));
}
}

/// libvirt subcommands for managing bootc disk images and domains
#[derive(Debug, Subcommand)]
pub enum LibvirtSubcommands {
Expand Down
3 changes: 3 additions & 0 deletions crates/kit/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//! Bootc Virtualization Kit (bcvk) - A toolkit for bootc containers and local virtualization

use std::ffi::OsString;

use cap_std_ext::cap_std::fs::Dir;
Expand Down Expand Up @@ -36,6 +38,7 @@ mod to_disk;
mod utils;
mod xml_utils;

/// Default state directory for bcvk container data
pub const CONTAINER_STATEDIR: &str = "/var/lib/bcvk";

/// A comprehensive toolkit for bootc containers and local virtualization.
Expand Down
4 changes: 4 additions & 0 deletions crates/kit/src/qemu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ fn allocate_vsock_cid(vhost_fd: File) -> Result<(OwnedFd, u32)> {
const VHOST_VSOCK_SET_GUEST_CID: libc::c_ulong = 0x4008af60;

let cid = candidate_cid as u64;
// SAFETY: ioctl is unsafe but we're passing valid file descriptor and pointer
#[allow(unsafe_code)]
let result = unsafe {
match libc::ioctl(
vhost_fd.as_raw_fd(),
Expand Down Expand Up @@ -409,6 +411,7 @@ fn spawn(

let mut cmd = Command::new(qemu);
// SAFETY: This API is safe to call in a forked child.
#[allow(unsafe_code)]
unsafe {
cmd.pre_exec(|| {
rustix::process::set_parent_process_death_signal(Some(rustix::process::Signal::TERM))
Expand Down Expand Up @@ -968,6 +971,7 @@ pub async fn spawn_virtiofsd_async(config: &VirtiofsConfig) -> Result<tokio::pro

let mut cmd = tokio::process::Command::new(virtiofsd_binary);
// SAFETY: This API is safe to call in a forked child.
#[allow(unsafe_code)]
unsafe {
cmd.pre_exec(|| {
rustix::process::set_parent_process_death_signal(Some(rustix::process::Signal::TERM))
Expand Down
8 changes: 8 additions & 0 deletions crates/kit/src/qemu_img.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,21 @@ use std::process::Command;
#[serde(rename_all = "kebab-case")]
#[allow(dead_code)]
pub struct QemuImgInfo {
/// Virtual size of the disk image in bytes
pub virtual_size: u64,
/// Path to the disk image file
pub filename: String,
/// Image format (e.g., "qcow2", "raw")
pub format: String,
/// Actual size on disk in bytes (if available)
pub actual_size: Option<u64>,
/// Cluster size in bytes (for formats like qcow2)
pub cluster_size: Option<u64>,
/// Backing file name (if this is a snapshot)
pub backing_filename: Option<String>,
/// Full path to backing file (if this is a snapshot)
pub full_backing_filename: Option<String>,
/// Whether the image is marked as dirty
pub dirty_flag: Option<bool>,
}

Expand Down
2 changes: 2 additions & 0 deletions crates/kit/src/run_ephemeral_ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ pub fn wait_for_vm_ssh(
"/var/lib/bcvk/entrypoint",
"monitor-status",
]);
// SAFETY: This API is safe to call in a forked child.
#[allow(unsafe_code)]
unsafe {
cmd.pre_exec(|| {
rustix::process::set_parent_process_death_signal(Some(rustix::process::Signal::TERM))
Expand Down
Loading