use super::{
CommandDefinition, CommandExecutionError, CommandExecutionResult, ExecutorCapabilities,
ResourceUsage, default_resource_usage,
};
use crate::client::ApiClient;
use std::collections::HashMap;
use std::time::{Duration, Instant};
pub struct FirecrackerExecutor {
api_client: Option<ApiClient>,
default_timeout: Duration,
vm_settings: VmSettings,
}
#[derive(Debug, Clone)]
pub struct VmSettings {
pub memory_mb: u64,
pub vcpu_count: u8,
pub disk_mb: u64,
pub network_enabled: bool,
pub root_fs: String,
}
impl Default for VmSettings {
fn default() -> Self {
Self {
memory_mb: 512,
vcpu_count: 1,
disk_mb: 1024,
network_enabled: false,
root_fs: "ubuntu:22.04".to_string(), }
}
}
impl FirecrackerExecutor {
pub fn new() -> Self {
Self {
api_client: None,
default_timeout: Duration::from_secs(300), vm_settings: VmSettings::default(),
}
}
pub fn with_api_client(api_client: ApiClient) -> Self {
Self {
api_client: Some(api_client),
default_timeout: Duration::from_secs(300),
vm_settings: VmSettings::default(),
}
}
pub fn with_vm_settings(mut self, settings: VmSettings) -> Self {
self.vm_settings = settings;
self
}
async fn prepare_vm(&self, command: &str) -> Result<String, CommandExecutionError> {
let api_client = self.api_client.as_ref().ok_or_else(|| {
CommandExecutionError::VmExecutionError("No API client available".to_string())
})?;
let vm_id = format!(
"firecracker-{}-{}",
command.replace(['/', ' '], "-"),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after Unix epoch")
.as_secs()
);
let _response = api_client.get_vm_status(&vm_id).await.map_err(|_| {
CommandExecutionError::VmExecutionError(format!("VM '{}' not available", vm_id))
})?;
Ok(vm_id)
}
async fn execute_in_vm(
&self,
vm_id: &str,
command: &str,
args: &[String],
_timeout: Duration,
) -> Result<CommandExecutionResult, CommandExecutionError> {
let api_client = self.api_client.as_ref().ok_or_else(|| {
CommandExecutionError::VmExecutionError("No API client available".to_string())
})?;
let start_time = Instant::now();
let full_command = format!("{} {}", command, args.join(" "));
let language = self.detect_language(command);
let response = api_client
.execute_vm_code(&full_command, &language, Some(vm_id))
.await
.map_err(|e| {
CommandExecutionError::VmExecutionError(format!("VM execution failed: {}", e))
})?;
let duration_ms = start_time.elapsed().as_millis() as u64;
Ok(CommandExecutionResult {
command: full_command,
execution_mode: super::ExecutionMode::Firecracker,
exit_code: response.exit_code,
stdout: response.stdout.clone(),
stderr: response.stderr.clone(),
duration_ms,
resource_usage: Some(self.calculate_resource_usage(&response)),
})
}
fn detect_language(&self, command: &str) -> String {
if command.contains("python") || command.contains("pip") {
"python".to_string()
} else if command.contains("node") || command.contains("npm") {
"javascript".to_string()
} else if command.contains("java") || command.contains("javac") {
"java".to_string()
} else if command.contains("go") {
"go".to_string()
} else if command.contains("rust") || command.contains("cargo") {
"rust".to_string()
} else {
"bash".to_string() }
}
fn calculate_resource_usage(
&self,
_response: &crate::client::VmExecuteResponse,
) -> ResourceUsage {
default_resource_usage()
}
async fn cleanup_vm(&self, vm_id: &str) -> Result<(), CommandExecutionError> {
if let Some(api_client) = &self.api_client {
let _response = api_client.get_vm_status(vm_id).await.map_err(|e| {
CommandExecutionError::VmExecutionError(format!("Failed to check VM status: {}", e))
})?;
}
Ok(())
}
fn parse_command(
&self,
command_str: &str,
) -> Result<(String, Vec<String>), CommandExecutionError> {
let parts: Vec<&str> = command_str.split_whitespace().collect();
if parts.is_empty() {
return Err(CommandExecutionError::VmExecutionError(
"Empty command".to_string(),
));
}
let command = parts[0].to_string();
let args: Vec<String> = parts[1..].iter().map(|&s| s.to_string()).collect();
Ok((command, args))
}
fn validate_vm_command(
&self,
command: &str,
args: &[String],
) -> Result<(), CommandExecutionError> {
let vm_incompatible_commands = [
"systemctl",
"service",
"init",
"shutdown",
"reboot",
"mount",
"umount",
"fdisk",
"mkfs",
"iptables",
"ufw",
"firewall",
];
if vm_incompatible_commands.contains(&command) {
return Err(CommandExecutionError::VmExecutionError(format!(
"Command '{}' is not compatible with VM execution",
command
)));
}
let total_length = command.len() + args.iter().map(|a| a.len()).sum::<usize>();
if total_length > 100_000 {
return Err(CommandExecutionError::VmExecutionError(
"Command too long for VM execution".to_string(),
));
}
Ok(())
}
}
#[async_trait::async_trait]
impl super::CommandExecutor for FirecrackerExecutor {
async fn execute_command(
&self,
definition: &CommandDefinition,
parameters: &HashMap<String, String>,
) -> Result<CommandExecutionResult, CommandExecutionError> {
let command_str = parameters.get("command").ok_or_else(|| {
CommandExecutionError::VmExecutionError("Missing 'command' parameter".to_string())
})?;
let (command, args) = self.parse_command(command_str)?;
self.validate_vm_command(&command, &args)?;
let vm_id = self.prepare_vm(&command).await?;
let timeout = definition
.timeout
.map(Duration::from_secs)
.unwrap_or(self.default_timeout);
let execution_result = tokio::time::timeout(
timeout,
self.execute_in_vm(&vm_id, &command, &args, timeout),
)
.await
.map_err(|_| CommandExecutionError::Timeout(timeout.as_secs()))??;
let _ = self.cleanup_vm(&vm_id).await;
Ok(execution_result)
}
fn supports_mode(&self, mode: &super::ExecutionMode) -> bool {
matches!(mode, super::ExecutionMode::Firecracker)
}
fn capabilities(&self) -> ExecutorCapabilities {
ExecutorCapabilities {
supports_resource_limits: true,
supports_network_access: self.vm_settings.network_enabled,
supports_file_system: true,
max_concurrent_commands: Some(5), default_timeout: Some(self.default_timeout.as_secs()),
}
}
}
impl Default for FirecrackerExecutor {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::modes::LocalExecutor;
#[test]
fn test_language_detection() {
let executor = LocalExecutor::new();
drop(executor);
}
#[test]
fn test_vm_command_validation() {
let executor = LocalExecutor::new();
drop(executor);
}
#[test]
fn test_command_parsing() {
let executor = LocalExecutor::new();
drop(executor);
}
#[test]
fn test_vm_settings_default() {
let settings = VmSettings::default();
assert_eq!(settings.memory_mb, 512);
assert_eq!(settings.vcpu_count, 1);
assert_eq!(settings.disk_mb, 1024);
assert!(!settings.network_enabled);
assert_eq!(settings.root_fs, "ubuntu:22.04");
}
}