Volt VMM (Neutron Stardust): source-available under AGPSL v5.0
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
This commit is contained in:
344
tests/integration/boot_test.rs
Normal file
344
tests/integration/boot_test.rs
Normal file
@@ -0,0 +1,344 @@
|
||||
//! 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);
|
||||
}
|
||||
3
tests/integration/mod.rs
Normal file
3
tests/integration/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
//! Integration tests for Volt
|
||||
|
||||
mod boot_test;
|
||||
Reference in New Issue
Block a user