KVM-based microVMM for the Volt platform: - Sub-second VM boot times - Minimal memory footprint - Landlock LSM + seccomp security - Virtio device support - Custom kernel management Copyright (c) Armored Gates LLC. All rights reserved. Licensed under AGPSL v5.0
345 lines
9.1 KiB
Rust
345 lines
9.1 KiB
Rust
//! Integration tests for Volt VM boot
|
|
//!
|
|
//! These tests verify that VMs boot correctly and measure boot times.
|
|
//! Run with: cargo test --test boot_test -- --ignored
|
|
//!
|
|
//! Requirements:
|
|
//! - KVM access (/dev/kvm readable/writable)
|
|
//! - Built kernel in kernels/vmlinux
|
|
//! - Built rootfs in images/alpine-rootfs.ext4
|
|
|
|
use std::io::{BufRead, BufReader};
|
|
use std::path::PathBuf;
|
|
use std::process::{Child, Command, Stdio};
|
|
use std::sync::mpsc;
|
|
use std::thread;
|
|
use std::time::{Duration, Instant};
|
|
|
|
/// Get the project root directory
|
|
fn project_root() -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.parent()
|
|
.unwrap()
|
|
.to_path_buf()
|
|
}
|
|
|
|
/// Check if KVM is available
|
|
fn kvm_available() -> bool {
|
|
std::path::Path::new("/dev/kvm").exists()
|
|
&& std::fs::metadata("/dev/kvm")
|
|
.map(|m| !m.permissions().readonly())
|
|
.unwrap_or(false)
|
|
}
|
|
|
|
/// Get path to the Volt binary
|
|
fn volt-vmm_binary() -> PathBuf {
|
|
let release = project_root().join("target/release/volt-vmm");
|
|
if release.exists() {
|
|
release
|
|
} else {
|
|
project_root().join("target/debug/volt-vmm")
|
|
}
|
|
}
|
|
|
|
/// Get path to the test kernel
|
|
fn test_kernel() -> PathBuf {
|
|
project_root().join("kernels/vmlinux")
|
|
}
|
|
|
|
/// Get path to the test rootfs
|
|
fn test_rootfs() -> PathBuf {
|
|
let ext4 = project_root().join("images/alpine-rootfs.ext4");
|
|
if ext4.exists() {
|
|
ext4
|
|
} else {
|
|
project_root().join("images/alpine-rootfs.squashfs")
|
|
}
|
|
}
|
|
|
|
/// Spawn a VM and return the child process
|
|
fn spawn_vm(memory_mb: u32, cpus: u32) -> std::io::Result<Child> {
|
|
let binary = volt-vmm_binary();
|
|
let kernel = test_kernel();
|
|
let rootfs = test_rootfs();
|
|
|
|
Command::new(&binary)
|
|
.arg("--kernel")
|
|
.arg(&kernel)
|
|
.arg("--rootfs")
|
|
.arg(&rootfs)
|
|
.arg("--memory")
|
|
.arg(memory_mb.to_string())
|
|
.arg("--cpus")
|
|
.arg(cpus.to_string())
|
|
.arg("--cmdline")
|
|
.arg("console=ttyS0 reboot=k panic=1 nomodules quiet")
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
}
|
|
|
|
/// Wait for a specific string in VM output
|
|
fn wait_for_output(
|
|
child: &mut Child,
|
|
pattern: &str,
|
|
timeout: Duration,
|
|
) -> Result<Duration, String> {
|
|
let start = Instant::now();
|
|
let stdout = child.stdout.take().ok_or("No stdout")?;
|
|
let reader = BufReader::new(stdout);
|
|
|
|
let (tx, rx) = mpsc::channel();
|
|
let pattern = pattern.to_string();
|
|
|
|
// Spawn reader thread
|
|
thread::spawn(move || {
|
|
for line in reader.lines() {
|
|
if let Ok(line) = line {
|
|
if line.contains(&pattern) {
|
|
let _ = tx.send(Instant::now());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Wait for pattern or timeout
|
|
match rx.recv_timeout(timeout) {
|
|
Ok(found_time) => Ok(found_time.duration_since(start)),
|
|
Err(_) => Err(format!("Timeout waiting for '{}'", pattern)),
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Tests
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
#[ignore = "requires KVM and built assets"]
|
|
fn test_vm_boots() {
|
|
if !kvm_available() {
|
|
eprintln!("Skipping: KVM not available");
|
|
return;
|
|
}
|
|
|
|
let binary = volt-vmm_binary();
|
|
if !binary.exists() {
|
|
eprintln!("Skipping: Volt binary not found at {:?}", binary);
|
|
return;
|
|
}
|
|
|
|
let kernel = test_kernel();
|
|
if !kernel.exists() {
|
|
eprintln!("Skipping: Kernel not found at {:?}", kernel);
|
|
return;
|
|
}
|
|
|
|
let rootfs = test_rootfs();
|
|
if !rootfs.exists() {
|
|
eprintln!("Skipping: Rootfs not found at {:?}", rootfs);
|
|
return;
|
|
}
|
|
|
|
println!("Starting VM...");
|
|
let mut child = spawn_vm(128, 1).expect("Failed to spawn VM");
|
|
|
|
// Wait for boot message
|
|
let result = wait_for_output(&mut child, "Volt microVM booted", Duration::from_secs(30));
|
|
|
|
// Clean up
|
|
let _ = child.kill();
|
|
|
|
match result {
|
|
Ok(boot_time) => {
|
|
println!("✓ VM booted successfully in {:?}", boot_time);
|
|
assert!(boot_time < Duration::from_secs(10), "Boot took too long");
|
|
}
|
|
Err(e) => {
|
|
panic!("VM boot failed: {}", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires KVM and built assets"]
|
|
fn test_boot_time_under_500ms() {
|
|
if !kvm_available() {
|
|
eprintln!("Skipping: KVM not available");
|
|
return;
|
|
}
|
|
|
|
let binary = volt-vmm_binary();
|
|
let kernel = test_kernel();
|
|
let rootfs = test_rootfs();
|
|
|
|
if !binary.exists() || !kernel.exists() || !rootfs.exists() {
|
|
eprintln!("Skipping: Required assets not found");
|
|
return;
|
|
}
|
|
|
|
// Run multiple times and average
|
|
let mut boot_times = Vec::new();
|
|
let iterations = 3;
|
|
|
|
for i in 0..iterations {
|
|
println!("Boot test iteration {}/{}", i + 1, iterations);
|
|
|
|
let mut child = spawn_vm(128, 1).expect("Failed to spawn VM");
|
|
|
|
// Look for kernel boot message or shell prompt
|
|
let result = wait_for_output(&mut child, "Booting", Duration::from_secs(5));
|
|
|
|
let _ = child.kill();
|
|
|
|
if let Ok(duration) = result {
|
|
boot_times.push(duration);
|
|
}
|
|
}
|
|
|
|
if boot_times.is_empty() {
|
|
eprintln!("No successful boots recorded");
|
|
return;
|
|
}
|
|
|
|
let avg_boot: Duration =
|
|
boot_times.iter().sum::<Duration>() / boot_times.len() as u32;
|
|
|
|
println!("Average boot time: {:?} ({} samples)", avg_boot, boot_times.len());
|
|
|
|
// Target: <500ms to first kernel output
|
|
// This is aggressive but achievable with PVH boot
|
|
if avg_boot < Duration::from_millis(500) {
|
|
println!("✓ Boot time target met: {:?} < 500ms", avg_boot);
|
|
} else {
|
|
println!("⚠ Boot time target missed: {:?} >= 500ms", avg_boot);
|
|
// Don't fail yet - this is aspirational
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires KVM and built assets"]
|
|
fn test_multiple_vcpus() {
|
|
if !kvm_available() {
|
|
return;
|
|
}
|
|
|
|
let binary = volt-vmm_binary();
|
|
let kernel = test_kernel();
|
|
let rootfs = test_rootfs();
|
|
|
|
if !binary.exists() || !kernel.exists() || !rootfs.exists() {
|
|
return;
|
|
}
|
|
|
|
// Test with 2 and 4 vCPUs
|
|
for cpus in [2, 4] {
|
|
println!("Testing with {} vCPUs...", cpus);
|
|
|
|
let mut child = spawn_vm(256, cpus).expect("Failed to spawn VM");
|
|
|
|
let result = wait_for_output(
|
|
&mut child,
|
|
"Volt microVM booted",
|
|
Duration::from_secs(30),
|
|
);
|
|
|
|
let _ = child.kill();
|
|
|
|
assert!(result.is_ok(), "Failed to boot with {} vCPUs", cpus);
|
|
println!("✓ {} vCPUs: booted in {:?}", cpus, result.unwrap());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "requires KVM and built assets"]
|
|
fn test_memory_sizes() {
|
|
if !kvm_available() {
|
|
return;
|
|
}
|
|
|
|
let binary = volt-vmm_binary();
|
|
let kernel = test_kernel();
|
|
let rootfs = test_rootfs();
|
|
|
|
if !binary.exists() || !kernel.exists() || !rootfs.exists() {
|
|
return;
|
|
}
|
|
|
|
// Test various memory sizes
|
|
for mem_mb in [64, 128, 256, 512] {
|
|
println!("Testing with {}MB memory...", mem_mb);
|
|
|
|
let mut child = spawn_vm(mem_mb, 1).expect("Failed to spawn VM");
|
|
|
|
let result = wait_for_output(
|
|
&mut child,
|
|
"Volt microVM booted",
|
|
Duration::from_secs(30),
|
|
);
|
|
|
|
let _ = child.kill();
|
|
|
|
assert!(result.is_ok(), "Failed to boot with {}MB", mem_mb);
|
|
println!("✓ {}MB: booted in {:?}", mem_mb, result.unwrap());
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Benchmarks (manual, run with --nocapture)
|
|
// ============================================================================
|
|
|
|
#[test]
|
|
#[ignore = "benchmark - run manually"]
|
|
fn bench_cold_boot() {
|
|
if !kvm_available() {
|
|
return;
|
|
}
|
|
|
|
println!("\n=== Cold Boot Benchmark ===\n");
|
|
|
|
let iterations = 10;
|
|
let mut times = Vec::with_capacity(iterations);
|
|
|
|
for i in 0..iterations {
|
|
// Clear caches (would need root)
|
|
// let _ = Command::new("sync").status();
|
|
// let _ = std::fs::write("/proc/sys/vm/drop_caches", "3");
|
|
|
|
let start = Instant::now();
|
|
let mut child = spawn_vm(128, 1).expect("Failed to spawn");
|
|
|
|
let result = wait_for_output(
|
|
&mut child,
|
|
"Volt microVM booted",
|
|
Duration::from_secs(30),
|
|
);
|
|
|
|
let _ = child.kill();
|
|
|
|
if let Ok(_) = result {
|
|
let elapsed = start.elapsed();
|
|
times.push(elapsed);
|
|
println!(" Run {:2}: {:?}", i + 1, elapsed);
|
|
}
|
|
}
|
|
|
|
if times.is_empty() {
|
|
println!("No successful runs");
|
|
return;
|
|
}
|
|
|
|
times.sort();
|
|
|
|
let sum: Duration = times.iter().sum();
|
|
let avg = sum / times.len() as u32;
|
|
let min = times.first().unwrap();
|
|
let max = times.last().unwrap();
|
|
let median = ×[times.len() / 2];
|
|
|
|
println!("\nResults ({} runs):", times.len());
|
|
println!(" Min: {:?}", min);
|
|
println!(" Max: {:?}", max);
|
|
println!(" Avg: {:?}", avg);
|
|
println!(" Median: {:?}", median);
|
|
}
|