use serde::{Deserialize, Serialize};
use crate::lifecycle::SandboxConfig;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct VmConfig {
pub boot_source: BootSource,
pub drives: Vec<Drive>,
pub machine_config: MachineConfig,
#[serde(skip_serializing_if = "Option::is_none")]
pub network_interfaces: Option<Vec<NetworkInterface>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vsock: Option<VsockConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BootSource {
pub kernel_image_path: String,
pub boot_args: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Drive {
pub drive_id: String,
pub path_on_host: String,
pub is_root_device: bool,
pub is_read_only: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MachineConfig {
pub vcpu_count: u32,
pub mem_size_mib: u64,
pub smt: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkInterface {
pub iface_id: String,
pub guest_mac: String,
pub host_dev_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VsockConfig {
pub guest_cid: u32,
pub uds_path: String,
}
const DEFAULT_BOOT_ARGS: &str =
"console=ttyS0 reboot=k panic=1 pci=off quiet loglevel=1 init=/sbin/overlay-init";
#[derive(Debug, Clone)]
pub struct FirecrackerConfig {
pub kernel_path: String,
pub rootfs_path: String,
pub vcpu_count: u32,
pub mem_size_mib: u64,
pub boot_args: Option<String>,
pub vsock_cid: Option<u32>,
}
impl Default for FirecrackerConfig {
fn default() -> Self {
Self {
kernel_path: "/var/lib/kavach/vmlinux".into(),
rootfs_path: "/var/lib/kavach/rootfs.ext4".into(),
vcpu_count: 1,
mem_size_mib: 128,
boot_args: None,
vsock_cid: None,
}
}
}
impl FirecrackerConfig {
pub fn from_sandbox_config(config: &SandboxConfig) -> Self {
let mut fc = Self::default();
if let Some(mb) = config.policy.memory_limit_mb {
fc.mem_size_mib = mb;
}
if let Some(cpu) = config.policy.cpu_limit {
fc.vcpu_count = (cpu.ceil() as u32).max(1);
}
fc
}
pub fn to_vm_config(&self, workdir: &std::path::Path) -> VmConfig {
let vsock_path = workdir.join("vsock.sock");
VmConfig {
boot_source: BootSource {
kernel_image_path: self.kernel_path.clone(),
boot_args: self
.boot_args
.clone()
.unwrap_or_else(|| DEFAULT_BOOT_ARGS.to_string()),
},
drives: vec![Drive {
drive_id: "rootfs".into(),
path_on_host: self.rootfs_path.clone(),
is_root_device: true,
is_read_only: true,
}],
machine_config: MachineConfig {
vcpu_count: self.vcpu_count,
mem_size_mib: self.mem_size_mib,
smt: false,
},
network_interfaces: None, vsock: self.vsock_cid.map(|cid| VsockConfig {
guest_cid: cid,
uds_path: vsock_path.to_string_lossy().into_owned(),
}),
}
}
pub fn write_config(&self, workdir: &std::path::Path) -> crate::Result<std::path::PathBuf> {
let vm_config = self.to_vm_config(workdir);
let config_path = workdir.join("firecracker.json");
let json = serde_json::to_string_pretty(&vm_config)
.map_err(|e| crate::KavachError::CreationFailed(format!("FC config: {e}")))?;
std::fs::write(&config_path, json)
.map_err(|e| crate::KavachError::CreationFailed(format!("write FC config: {e}")))?;
Ok(config_path)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::Backend;
#[test]
fn default_config() {
let fc = FirecrackerConfig::default();
assert_eq!(fc.vcpu_count, 1);
assert_eq!(fc.mem_size_mib, 128);
assert!(fc.vsock_cid.is_none());
}
#[test]
fn from_sandbox_config_memory() {
let mut config = SandboxConfig::builder()
.backend(Backend::Firecracker)
.build();
config.policy.memory_limit_mb = Some(256);
let fc = FirecrackerConfig::from_sandbox_config(&config);
assert_eq!(fc.mem_size_mib, 256);
}
#[test]
fn from_sandbox_config_cpu() {
let mut config = SandboxConfig::builder()
.backend(Backend::Firecracker)
.build();
config.policy.cpu_limit = Some(2.5);
let fc = FirecrackerConfig::from_sandbox_config(&config);
assert_eq!(fc.vcpu_count, 3); }
#[test]
fn vm_config_json() {
let fc = FirecrackerConfig::default();
let vm = fc.to_vm_config(std::path::Path::new("/tmp"));
let json = serde_json::to_string(&vm).unwrap();
assert!(json.contains("boot-source"));
assert!(json.contains("machine-config"));
assert!(json.contains("rootfs"));
}
#[test]
fn vm_config_with_vsock() {
let fc = FirecrackerConfig {
vsock_cid: Some(3),
..Default::default()
};
let vm = fc.to_vm_config(std::path::Path::new("/tmp"));
assert!(vm.vsock.is_some());
assert_eq!(vm.vsock.unwrap().guest_cid, 3);
}
#[test]
fn vm_config_no_network_by_default() {
let fc = FirecrackerConfig::default();
let vm = fc.to_vm_config(std::path::Path::new("/tmp"));
assert!(vm.network_interfaces.is_none());
}
#[test]
fn serde_roundtrip() {
let fc = FirecrackerConfig::default();
let vm = fc.to_vm_config(std::path::Path::new("/tmp"));
let json = serde_json::to_string(&vm).unwrap();
let back: VmConfig = serde_json::from_str(&json).unwrap();
assert_eq!(back.machine_config.vcpu_count, 1);
assert_eq!(back.machine_config.mem_size_mib, 128);
}
}