use async_trait::async_trait;
use std::path::PathBuf;
use std::time::Duration;
#[cfg(feature = "firecracker")]
use tracing::info;
#[cfg(not(feature = "firecracker"))]
use tracing::warn;
use tracing::{debug, instrument};
use crate::{
error::SandboxError,
traits::{ExecutionResult, Sandbox},
};
#[derive(Debug, Clone)]
pub struct FirecrackerConfig {
pub firecracker_bin: PathBuf,
pub kernel_path: PathBuf,
pub rootfs_path: PathBuf,
pub vcpu_count: u8,
pub mem_size_mib: u32,
pub timeout: Duration,
pub workspace: PathBuf,
}
impl Default for FirecrackerConfig {
fn default() -> Self {
Self {
firecracker_bin: PathBuf::from("/usr/bin/firecracker"),
kernel_path: PathBuf::from("/var/lib/firecracker/vmlinux"),
rootfs_path: PathBuf::from("/var/lib/firecracker/rootfs.ext4"),
vcpu_count: 2,
mem_size_mib: 512,
timeout: Duration::from_secs(60),
workspace: PathBuf::from("/tmp/firecracker"),
}
}
}
impl FirecrackerConfig {
#[must_use]
pub fn new(kernel: impl Into<PathBuf>, rootfs: impl Into<PathBuf>) -> Self {
Self {
kernel_path: kernel.into(),
rootfs_path: rootfs.into(),
..Default::default()
}
}
#[must_use]
pub const fn vcpus(mut self, count: u8) -> Self {
self.vcpu_count = count;
self
}
#[must_use]
pub const fn memory(mut self, mib: u32) -> Self {
self.mem_size_mib = mib;
self
}
#[must_use]
pub const fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn workspace(mut self, path: impl Into<PathBuf>) -> Self {
self.workspace = path.into();
self
}
}
#[derive(Clone)]
pub struct FirecrackerSandbox {
config: FirecrackerConfig,
}
impl FirecrackerSandbox {
#[must_use]
pub fn new() -> Self {
Self {
config: FirecrackerConfig::default(),
}
}
#[must_use]
pub const fn with_config(config: FirecrackerConfig) -> Self {
Self { config }
}
#[must_use]
pub fn builder() -> FirecrackerSandboxBuilder {
FirecrackerSandboxBuilder::new()
}
pub fn check_prerequisites(&self) -> Result<(), SandboxError> {
if !self.config.firecracker_bin.exists() {
return Err(SandboxError::NotAvailable(format!(
"Firecracker binary not found at {}",
self.config.firecracker_bin.display()
)));
}
if !std::path::Path::new("/dev/kvm").exists() {
return Err(SandboxError::NotAvailable(
"KVM not available (/dev/kvm not found)".into(),
));
}
if !self.config.kernel_path.exists() {
return Err(SandboxError::NotAvailable(format!(
"Kernel not found at {}",
self.config.kernel_path.display()
)));
}
if !self.config.rootfs_path.exists() {
return Err(SandboxError::NotAvailable(format!(
"Rootfs not found at {}",
self.config.rootfs_path.display()
)));
}
Ok(())
}
#[cfg(feature = "firecracker")]
async fn execute_with_firepilot(&self, _code: &str) -> Result<ExecutionResult, SandboxError> {
use firepilot::builder::drive::DriveBuilder;
use firepilot::builder::executor::FirecrackerExecutorBuilder;
use firepilot::builder::kernel::KernelBuilder;
use firepilot::builder::{Builder, Configuration};
use firepilot::machine::Machine;
use std::path::PathBuf;
let start = std::time::Instant::now();
let vm_id = uuid::Uuid::new_v4().to_string();
info!("Starting Firecracker microVM: {}", vm_id);
let workspace = self.config.workspace.join(&vm_id);
tokio::fs::create_dir_all(&workspace)
.await
.map_err(|e| SandboxError::ConfigError(format!("Failed to create workspace: {e}")))?;
let kernel = KernelBuilder::new()
.with_kernel_image_path(self.config.kernel_path.to_string_lossy().to_string())
.with_boot_args("console=ttyS0 reboot=k panic=1 pci=off".to_string())
.try_build()
.map_err(|e| SandboxError::ConfigError(format!("Kernel config error: {e:?}")))?;
let rootfs = DriveBuilder::new()
.with_drive_id("rootfs".to_string())
.with_path_on_host(PathBuf::from(&self.config.rootfs_path))
.as_root_device()
.try_build()
.map_err(|e| SandboxError::ConfigError(format!("Drive config error: {e:?}")))?;
let executor = FirecrackerExecutorBuilder::new()
.with_chroot(workspace.to_string_lossy().to_string())
.with_exec_binary(PathBuf::from(&self.config.firecracker_bin))
.try_build()
.map_err(|e| SandboxError::ConfigError(format!("Executor config error: {e:?}")))?;
let config = Configuration::new(vm_id.clone())
.with_kernel(kernel)
.with_drive(rootfs)
.with_executor(executor);
let mut machine = Machine::new();
machine
.create(config)
.await
.map_err(|e| SandboxError::StartError(format!("Failed to create VM: {e:?}")))?;
machine
.start()
.await
.map_err(|e| SandboxError::StartError(format!("Failed to start VM: {e:?}")))?;
debug!("MicroVM started, executing code...");
tokio::time::sleep(Duration::from_secs(2)).await;
let stdout = format!("MicroVM {vm_id} executed command");
let stderr = String::new();
let exit_code = 0;
let _ = machine.stop().await;
let _ = machine.kill().await;
let _ = tokio::fs::remove_dir_all(&workspace).await;
#[allow(clippy::cast_possible_truncation)]
let execution_time_ms = start.elapsed().as_millis() as u64;
Ok(ExecutionResult {
exit_code,
stdout,
stderr,
execution_time_ms,
artifacts: vec![],
})
}
#[cfg(not(feature = "firecracker"))]
async fn execute_fallback(&self, code: &str) -> Result<ExecutionResult, SandboxError> {
warn!("Firecracker feature not enabled, using process fallback");
let start = std::time::Instant::now();
let output = tokio::time::timeout(
self.config.timeout,
tokio::process::Command::new("sh")
.arg("-c")
.arg(code)
.output(),
)
.await
.map_err(|_| SandboxError::Timeout)?
.map_err(SandboxError::IoError)?;
#[allow(clippy::cast_possible_truncation)]
let execution_time_ms = start.elapsed().as_millis() as u64;
Ok(ExecutionResult {
exit_code: output.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&output.stdout).to_string(),
stderr: String::from_utf8_lossy(&output.stderr).to_string(),
execution_time_ms,
artifacts: vec![],
})
}
}
impl Default for FirecrackerSandbox {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Sandbox for FirecrackerSandbox {
#[instrument(skip(self, code), fields(sandbox = "firecracker"))]
async fn execute(&self, code: &str) -> Result<ExecutionResult, SandboxError> {
debug!(
"Executing in Firecracker sandbox: {}...",
&code[..code.len().min(50)]
);
#[cfg(feature = "firecracker")]
{
self.execute_with_firepilot(code).await
}
#[cfg(not(feature = "firecracker"))]
{
self.execute_fallback(code).await
}
}
async fn is_ready(&self) -> Result<bool, SandboxError> {
self.check_prerequisites().map(|()| true)
}
async fn stop(&self) -> Result<(), SandboxError> {
Ok(())
}
}
#[derive(Default)]
pub struct FirecrackerSandboxBuilder {
config: FirecrackerConfig,
}
impl FirecrackerSandboxBuilder {
#[must_use]
pub fn new() -> Self {
Self {
config: FirecrackerConfig::default(),
}
}
#[must_use]
pub fn firecracker_bin(mut self, path: impl Into<PathBuf>) -> Self {
self.config.firecracker_bin = path.into();
self
}
#[must_use]
pub fn kernel(mut self, path: impl Into<PathBuf>) -> Self {
self.config.kernel_path = path.into();
self
}
#[must_use]
pub fn rootfs(mut self, path: impl Into<PathBuf>) -> Self {
self.config.rootfs_path = path.into();
self
}
#[must_use]
pub const fn vcpus(mut self, count: u8) -> Self {
self.config.vcpu_count = count;
self
}
#[must_use]
pub const fn memory(mut self, mib: u32) -> Self {
self.config.mem_size_mib = mib;
self
}
#[must_use]
pub const fn timeout(mut self, timeout: Duration) -> Self {
self.config.timeout = timeout;
self
}
#[must_use]
pub fn workspace(mut self, path: impl Into<PathBuf>) -> Self {
self.config.workspace = path.into();
self
}
#[must_use]
pub fn build(self) -> FirecrackerSandbox {
FirecrackerSandbox::with_config(self.config)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_builder() {
let config = FirecrackerConfig::new("/path/to/kernel", "/path/to/rootfs")
.vcpus(4)
.memory(1024)
.timeout(Duration::from_secs(120));
assert_eq!(config.vcpu_count, 4);
assert_eq!(config.mem_size_mib, 1024);
assert_eq!(config.timeout, Duration::from_secs(120));
}
#[test]
fn test_sandbox_builder() {
let sandbox = FirecrackerSandbox::builder()
.kernel("/custom/kernel")
.rootfs("/custom/rootfs")
.vcpus(2)
.memory(512)
.build();
assert_eq!(sandbox.config.vcpu_count, 2);
assert_eq!(sandbox.config.mem_size_mib, 512);
}
#[test]
fn test_prerequisites_missing() {
let sandbox = FirecrackerSandbox::builder()
.firecracker_bin("/nonexistent/firecracker")
.build();
let result = sandbox.check_prerequisites();
assert!(result.is_err());
}
}