mod common;
use std::collections::HashMap;
use std::time::Duration;
use arcbox_vm::{
DefaultVmConfig, FirecrackerConfig, GrpcConfig, NetworkConfig, SandboxEvent, SandboxManager,
SandboxNetworkSpec, SandboxSpec, SandboxState, VmmConfig,
};
fn try_config(data_dir: &str) -> Option<VmmConfig> {
let binary = std::env::var("FC_BINARY").ok()?;
let kernel = std::env::var("FC_KERNEL").ok()?;
let rootfs = std::env::var("FC_ROOTFS").ok()?;
Some(VmmConfig {
firecracker: FirecrackerConfig {
binary,
jailer: None,
data_dir: data_dir.to_owned(),
log_level: Some("Error".into()),
no_seccomp: true,
seccomp_filter: None,
http_api_max_payload_size: None,
mmds_size_limit: None,
socket_timeout_secs: Some(15),
},
network: NetworkConfig {
cidr: "172.99.0.0/24".into(),
gateway: "172.99.0.1".into(),
dns: vec![],
},
grpc: GrpcConfig {
unix_socket: "/dev/null".into(),
tcp_addr: String::new(),
},
defaults: DefaultVmConfig {
vcpus: 1,
memory_mib: 256,
kernel,
rootfs,
boot_args: "console=ttyS0 reboot=k panic=1 pci=off init=/sbin/vm-agent".into(),
},
})
}
fn no_tap() -> SandboxSpec {
SandboxSpec {
network: SandboxNetworkSpec {
mode: "none".into(),
},
..Default::default()
}
}
async fn wait_for_event(
rx: &mut tokio::sync::broadcast::Receiver<SandboxEvent>,
id: &str,
action: &str,
) -> bool {
let deadline = tokio::time::Instant::now() + Duration::from_secs(30);
loop {
match tokio::time::timeout_at(deadline, rx.recv()).await {
Ok(Ok(ev)) if ev.sandbox_id == id => {
if ev.action == action {
return true;
}
if ev.action == "failed" {
eprintln!("sandbox {id} failed: {:?}", ev.attributes);
return false;
}
}
Ok(Ok(_)) => {}
Ok(Err(tokio::sync::broadcast::error::RecvError::Lagged(_))) => {}
_ => return false,
}
}
}
#[tokio::test]
#[ignore = "requires FC_BINARY/FC_KERNEL/FC_ROOTFS environment variables"]
async fn e2e_sandbox_basic_lifecycle() {
let dir = tempfile::tempdir().unwrap();
let Some(cfg) = try_config(dir.path().to_str().unwrap()) else {
eprintln!("SKIP e2e_sandbox_basic_lifecycle — FC_BINARY/FC_KERNEL/FC_ROOTFS not set");
return;
};
let mgr = SandboxManager::new(cfg).unwrap();
let mut events = mgr.subscribe_events();
let (id, _ip) = mgr.create_sandbox(no_tap()).await.unwrap();
assert!(
wait_for_event(&mut events, &id, "ready").await,
"sandbox did not reach ready state"
);
let info = mgr.inspect_sandbox(&id).unwrap();
assert_eq!(info.state, SandboxState::Ready);
mgr.stop_sandbox(&id, 5).await.unwrap();
let info = mgr.inspect_sandbox(&id).unwrap();
assert_eq!(info.state, SandboxState::Stopped);
mgr.remove_sandbox(&id, true).await.unwrap();
assert!(
mgr.inspect_sandbox(&id).is_err(),
"sandbox should be gone after remove"
);
}
#[tokio::test]
#[ignore = "requires FC_BINARY/FC_KERNEL/FC_ROOTFS environment variables"]
async fn e2e_event_broadcast_ready() {
let dir = tempfile::tempdir().unwrap();
let Some(cfg) = try_config(dir.path().to_str().unwrap()) else {
eprintln!("SKIP e2e_event_broadcast_ready — FC_BINARY/FC_KERNEL/FC_ROOTFS not set");
return;
};
let mgr = SandboxManager::new(cfg).unwrap();
let mut events = mgr.subscribe_events();
let (id, _) = mgr.create_sandbox(no_tap()).await.unwrap();
assert!(
wait_for_event(&mut events, &id, "ready").await,
"expected ready event for sandbox {id}"
);
mgr.remove_sandbox(&id, true).await.unwrap();
}
#[tokio::test]
#[ignore = "requires FC_BINARY/FC_KERNEL/FC_ROOTFS environment variables and root"]
async fn e2e_two_sandboxes_distinct_ips() {
#[cfg(target_os = "linux")]
if !common::is_root() {
eprintln!("SKIP e2e_two_sandboxes_distinct_ips — requires root");
return;
}
let dir = tempfile::tempdir().unwrap();
let Some(cfg) = try_config(dir.path().to_str().unwrap()) else {
eprintln!("SKIP e2e_two_sandboxes_distinct_ips — FC_BINARY/FC_KERNEL/FC_ROOTFS not set");
return;
};
let mgr = SandboxManager::new(cfg).unwrap();
let mut ev1 = mgr.subscribe_events();
let mut ev2 = mgr.subscribe_events();
let (id1, ip1) = mgr.create_sandbox(Default::default()).await.unwrap();
let (id2, ip2) = mgr.create_sandbox(Default::default()).await.unwrap();
assert_ne!(ip1, ip2, "sandboxes must receive distinct IP addresses");
assert!(
wait_for_event(&mut ev1, &id1, "ready").await,
"sandbox 1 did not reach ready"
);
assert!(
wait_for_event(&mut ev2, &id2, "ready").await,
"sandbox 2 did not reach ready"
);
mgr.remove_sandbox(&id1, true).await.unwrap();
mgr.remove_sandbox(&id2, true).await.unwrap();
}
#[tokio::test]
#[ignore = "requires FC_BINARY/FC_KERNEL/FC_ROOTFS environment variables and root"]
async fn e2e_sandbox_with_tap_network() {
#[cfg(target_os = "linux")]
if !common::is_root() {
eprintln!("SKIP e2e_sandbox_with_tap_network — requires root");
return;
}
let dir = tempfile::tempdir().unwrap();
let Some(cfg) = try_config(dir.path().to_str().unwrap()) else {
eprintln!("SKIP e2e_sandbox_with_tap_network — FC_BINARY/FC_KERNEL/FC_ROOTFS not set");
return;
};
let mgr = SandboxManager::new(cfg).unwrap();
let mut events = mgr.subscribe_events();
let (id, ip) = mgr.create_sandbox(Default::default()).await.unwrap();
assert!(!ip.is_empty(), "tap mode should assign an IP address");
assert!(
wait_for_event(&mut events, &id, "ready").await,
"sandbox did not reach ready"
);
#[cfg(target_os = "linux")]
let tap_name = {
let info = mgr.inspect_sandbox(&id).unwrap();
let net = info.network.expect("tap mode should populate network info");
assert!(
common::iface_exists(&net.tap_name),
"TAP {} should exist while sandbox is running",
net.tap_name
);
net.tap_name
};
mgr.remove_sandbox(&id, true).await.unwrap();
#[cfg(target_os = "linux")]
assert!(
!common::iface_exists(&tap_name),
"TAP {tap_name} should be removed after sandbox is removed"
);
}
#[tokio::test]
#[ignore = "requires FC_BINARY/FC_KERNEL/FC_ROOTFS environment variables and vm-agent in rootfs"]
async fn e2e_run_command() {
let dir = tempfile::tempdir().unwrap();
let Some(cfg) = try_config(dir.path().to_str().unwrap()) else {
eprintln!("SKIP e2e_run_command — FC_BINARY/FC_KERNEL/FC_ROOTFS not set");
return;
};
let mgr = SandboxManager::new(cfg).unwrap();
let mut events = mgr.subscribe_events();
let (id, _) = mgr.create_sandbox(no_tap()).await.unwrap();
assert!(
wait_for_event(&mut events, &id, "ready").await,
"sandbox did not reach ready"
);
let mut rx = mgr
.run_in_sandbox(
&id,
vec!["echo".into(), "hello from vm".into()],
HashMap::new(),
"/".into(),
"root".into(),
false,
None,
10,
)
.await
.unwrap();
let mut stdout = String::new();
let mut exit_code: i32 = -1;
while let Some(result) = rx.recv().await {
let chunk = result.unwrap();
match chunk.stream.as_str() {
"stdout" => stdout.push_str(&String::from_utf8_lossy(&chunk.data)),
"exit" => exit_code = chunk.exit_code,
_ => {}
}
}
assert!(
stdout.contains("hello from vm"),
"expected 'hello from vm' in stdout, got: {stdout:?}"
);
assert_eq!(exit_code, 0, "echo should exit with code 0");
mgr.remove_sandbox(&id, true).await.unwrap();
}