Core Concepts
This guide covers the fundamental concepts behind Capsa's design and how its components work together.
Architecture Overview
Capsa provides a unified Rust API that abstracts platform-specific hypervisor implementations behind a clean interface:
+-----------------------------------------------------------+
| User Code |
+-----------------------------------------------------------+
|
v
+-----------------------------------------------------------+
| capsa (public API) |
| Capsa, VmBuilder, VmHandle, VmConsole, VsockSocket |
+-----------------------------------------------------------+
|
v
+-----------------------------------------------------------+
| HypervisorBackend trait (internal) |
+-----------------------------------------------------------+
| |
v v
+------------------------+ +-----------------------------+
| Linux KVM Backend | | macOS Backend |
| (capsa-linux-kvm) | | (subprocess via vzd) |
+------------------------+ +-----------------------------+The abstraction layer allows your code to work unchanged across platforms. You write against the capsa API, and Capsa automatically selects the appropriate backend based on the platform and configuration.
VM Lifecycle
A VM progresses through a well-defined lifecycle from creation to termination.
Creating a VM
All VM creation starts with Capsa::vm(), which accepts a boot configuration and returns a builder:
use capsa::{Capsa, LinuxDirectBootConfig};
let config = LinuxDirectBootConfig::new("./kernel", "./initrd")
.with_root_disk("./rootfs.raw");
let builder = Capsa::vm(config);Building and Starting
Calling .build().await validates the configuration, starts the VM, and returns a handle:
let vm = Capsa::vm(config)
.cpus(2)
.memory_mb(1024)
.console_enabled()
.build()
.await?;The build step validates that:
- The backend supports all requested features
- Resource limits are within bounds
- Referenced files (kernel, disk images) exist and are accessible
VM States
A VM transitions through these states:
| State | Description |
|---|---|
Created | VM is configured but not yet started |
Starting | VM is in the process of booting |
Running | VM is running and accessible |
Stopping | Graceful shutdown in progress |
Stopped | VM has terminated (includes exit code) |
Failed | VM encountered an error (includes message) |
Check the current state with vm.status():
match vm.status() {
VmStatus::Running => println!("VM is running"),
VmStatus::Stopped { exit_code } => println!("Exited with: {:?}", exit_code),
VmStatus::Failed { message } => eprintln!("Error: {}", message),
_ => {}
}Stopping a VM
There are two ways to stop a VM:
Graceful shutdown with stop():
vm.stop().await?;This sends a shutdown request to the guest and waits up to 30 seconds. If the guest does not respond, it falls back to a force kill. Use stop_with_timeout() for a custom grace period.
Immediate termination with kill():
vm.kill().await?;This forcefully terminates the VM without waiting for the guest to shut down cleanly.
Waiting for Exit
Use wait() to block until the VM exits:
let status = vm.wait().await?;Or with a timeout:
if let Some(status) = vm.wait_timeout(Duration::from_secs(60)).await? {
println!("VM exited: {:?}", status);
} else {
println!("Timeout waiting for VM");
}Boot Methods
Capsa supports two boot methods, each suited for different use cases.
Linux Direct Boot
Boots the kernel directly without a bootloader. This is the fastest option for Linux-only workloads.
use capsa::{Capsa, LinuxDirectBootConfig};
let config = LinuxDirectBootConfig::new("./vmlinuz", "./initrd.img")
.with_root_disk("./rootfs.raw");
let vm = Capsa::vm(config).build().await?;Advantages:
- Fastest boot time (no BIOS/UEFI, no bootloader)
- Simple configuration (just kernel and initrd)
- Ideal for integration testing and ephemeral workloads
When to use:
- Linux-only environments
- Test VMs where boot speed matters
- Minimal/custom Linux systems
UEFI Boot
Boots from a disk containing an EFI bootloader. This is OS-agnostic and supports any operating system with a UEFI-compatible bootloader.
use capsa::{Capsa, UefiBootConfig};
let config = UefiBootConfig::new("./disk.raw")
.with_efi_variable_store("./nvram.efivarstore");
let vm = Capsa::vm(config).build().await?;Advantages:
- OS-agnostic (Linux, Windows, BSDs, etc.)
- Supports standard disk images
- EFI variable persistence across reboots
When to use:
- Non-Linux operating systems
- Standard disk images with existing bootloaders
- When you need EFI variable persistence
Builder Pattern
Capsa uses the builder pattern for fluent, readable VM configuration. Methods chain together and apply configuration incrementally.
Common Configuration Methods
let vm = Capsa::vm(config)
// Resource allocation
.cpus(4)
.memory_mb(2048)
// Console access
.console_enabled()
// Networking
.network(NetworkMode::Nat)
// Directory sharing
.share("./workspace", "/mnt/workspace", MountMode::ReadWrite)
// Host-guest communication
.vsock_listen(1024)
// Additional disks
.disk(DiskImage::new("./data.raw"))
.build()
.await?;Type Safety via Generics
The builder uses generic type parameters to enforce compile-time constraints:
VmBuilder<LinuxDirectBootConfig, No> // Single VM builder
VmBuilder<LinuxDirectBootConfig, Yes> // Pool builderThis ensures that pool-specific methods are only available on pool builders, and vice versa.
Handle Types
After building a VM, you interact with it through various handle types.
VmHandle
The primary interface for controlling a running VM:
let vm: VmHandle = Capsa::vm(config).build().await?;
// Check status
let status = vm.status();
// Access console
let console = vm.console().await?;
// Access vsock
if let Some(socket) = vm.vsock_socket(1024) {
let stream = socket.connect().await?;
}
// Lifecycle control
vm.stop().await?;VmConsole
Provides serial console interaction for automation:
let console = vm.console().await?;
// Wait for patterns
console.wait_for("login:").await?;
console.write_line("root").await?;
// Run commands
let output = console.exec("ls /", Duration::from_secs(5)).await?;
// Login helper
console.login("user", Some("password")).await?;INFO
Console access requires .console_enabled() on the builder.
VsockSocket
Enables host-guest communication via VM sockets:
// Configure during build
let vm = Capsa::vm(config)
.vsock_listen(1024)
.build()
.await?;
// Connect from host
if let Some(socket) = vm.vsock_socket(1024) {
let mut stream = socket.connect().await?;
stream.write_all(b"hello guest").await?;
}PooledVm
A VM obtained from a pool that auto-releases on drop:
let pool = Capsa::pool(config)
.cpus(2)
.memory_mb(512)
.build(5)
.await?;
// Reserve a VM
let vm: PooledVm = pool.reserve().await?;
// Use it like a regular VmHandle
let console = vm.console().await?;
// VM is killed and replaced when `vm` goes out of scopePooledVm implements Deref<Target = VmHandle>, so all VmHandle methods work directly.
Backend Selection
Capsa automatically selects the appropriate backend based on platform and feature flags.
Platform-Specific Backends
| Platform | Backend | Requirements |
|---|---|---|
| Linux | KVM | /dev/kvm access |
| macOS | Virtualization.framework | Apple Silicon or Intel Mac |
Automatic Selection
When you call .build(), Capsa:
- Queries available backends via
available_backends() - Checks each backend's availability (e.g.,
/dev/kvmexists) - Selects the first available backend
- Validates your configuration against the backend's capabilities
Capability Checking
You can query backend capabilities programmatically:
use capsa::capabilities::{available_backends, BackendCapabilities};
for backend in available_backends() {
if !backend.is_available() {
continue;
}
let caps = backend.capabilities();
println!("Backend: {}", backend.name());
println!(" Linux direct boot: {}", caps.boot_methods.linux_direct);
println!(" UEFI boot: {}", caps.boot_methods.uefi);
println!(" VirtioFS: {}", caps.share_mechanisms.virtio_fs);
println!(" Vsock: {}", caps.devices.vsock);
}This is useful for:
- Skipping tests when a feature is unavailable
- Choosing configuration options dynamically
- Debugging why a build fails
Feature Flags
Backend availability is controlled by Cargo features:
| Feature | Description |
|---|---|
linux-kvm | Linux KVM backend |
macos-subprocess | macOS Virtualization.framework |
Enable the appropriate feature for your target platform:
[dependencies]
capsa = { version = "...", features = ["linux-kvm"] }