use serde::{Deserialize, Serialize};
use crate::error::{Result, VmmError};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TokenBucketSpec {
pub size: i64,
pub refill_time_ms: i64,
pub one_time_burst: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RateLimitSpec {
pub bandwidth: Option<TokenBucketSpec>,
pub ops: Option<TokenBucketSpec>,
}
impl From<TokenBucketSpec> for fc_sdk::types::TokenBucket {
fn from(s: TokenBucketSpec) -> Self {
Self {
size: s.size,
refill_time: s.refill_time_ms,
one_time_burst: s.one_time_burst,
}
}
}
impl From<RateLimitSpec> for fc_sdk::types::RateLimiter {
fn from(s: RateLimitSpec) -> Self {
Self {
bandwidth: s.bandwidth.map(Into::into),
ops: s.ops.map(Into::into),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum IoEngine {
#[default]
Sync,
Async,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum CacheType {
#[default]
Unsafe,
Writeback,
}
impl From<IoEngine> for fc_sdk::types::DriveIoEngine {
fn from(e: IoEngine) -> Self {
match e {
IoEngine::Sync => Self::Sync,
IoEngine::Async => Self::Async,
}
}
}
impl From<CacheType> for fc_sdk::types::DriveCacheType {
fn from(c: CacheType) -> Self {
match c {
CacheType::Unsafe => Self::Unsafe,
CacheType::Writeback => Self::Writeback,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum CpuTemplateSpec {
C3,
T2,
T2S,
T2CL,
T2A,
V1N1,
}
impl From<CpuTemplateSpec> for fc_sdk::types::CpuTemplate {
fn from(t: CpuTemplateSpec) -> Self {
match t {
CpuTemplateSpec::C3 => Self::C3,
CpuTemplateSpec::T2 => Self::T2,
CpuTemplateSpec::T2S => Self::T2s,
CpuTemplateSpec::T2CL => Self::T2cl,
CpuTemplateSpec::T2A => Self::T2a,
CpuTemplateSpec::V1N1 => Self::V1n1,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum HugePagesSpec {
#[serde(rename = "2M")]
TwoMB,
}
impl From<HugePagesSpec> for fc_sdk::types::MachineConfigurationHugePages {
fn from(h: HugePagesSpec) -> Self {
match h {
HugePagesSpec::TwoMB => Self::X2m,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DriveSpec {
pub drive_id: String,
pub path: String,
pub readonly: bool,
#[serde(default)]
pub io_engine: IoEngine,
#[serde(default)]
pub cache_type: CacheType,
pub partuuid: Option<String>,
pub rate_limit: Option<RateLimitSpec>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BalloonSpec {
pub amount_mib: i64,
pub deflate_on_oom: bool,
pub stats_polling_interval_s: Option<i64>,
pub free_page_hinting: Option<bool>,
pub free_page_reporting: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VsockSpec {
pub guest_cid: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemoryHotplugSpec {
pub total_size_mib: i64,
pub slot_size_mib: Option<i64>,
pub block_size_mib: Option<i64>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
pub enum MmdsVersionSpec {
#[default]
V1,
V2,
}
impl From<MmdsVersionSpec> for fc_sdk::types::MmdsConfigVersion {
fn from(v: MmdsVersionSpec) -> Self {
match v {
MmdsVersionSpec::V1 => Self::V1,
MmdsVersionSpec::V2 => Self::V2,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MmdsSpec {
pub network_interfaces: Vec<String>,
#[serde(default)]
pub version: MmdsVersionSpec,
pub ipv4_address: Option<String>,
#[serde(default)]
pub imds_compat: bool,
pub initial_data: Option<serde_json::Map<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JailerConfig {
pub binary: String,
pub uid: u32,
pub gid: u32,
pub chroot_base_dir: Option<String>,
pub netns: Option<String>,
#[serde(default)]
pub new_pid_ns: bool,
pub cgroup_version: Option<String>,
pub parent_cgroup: Option<String>,
#[serde(default)]
pub resource_limits: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VmmConfig {
pub firecracker: FirecrackerConfig,
pub network: NetworkConfig,
pub grpc: GrpcConfig,
pub defaults: DefaultVmConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FirecrackerConfig {
pub binary: String,
#[serde(default)]
pub jailer: Option<JailerConfig>,
pub data_dir: String,
#[serde(default)]
pub log_level: Option<String>,
#[serde(default)]
pub no_seccomp: bool,
#[serde(default)]
pub seccomp_filter: Option<String>,
#[serde(default)]
pub http_api_max_payload_size: Option<usize>,
#[serde(default)]
pub mmds_size_limit: Option<usize>,
#[serde(default)]
pub socket_timeout_secs: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkConfig {
pub bridge: String,
pub cidr: String,
pub gateway: String,
pub dns: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GrpcConfig {
pub unix_socket: String,
pub tcp_addr: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DefaultVmConfig {
pub vcpus: u64,
pub memory_mib: u64,
pub kernel: String,
pub rootfs: String,
pub boot_args: String,
}
impl Default for VmmConfig {
fn default() -> Self {
Self {
firecracker: FirecrackerConfig {
binary: "/usr/bin/firecracker".into(),
jailer: None,
data_dir: "/var/lib/firecracker-vmm".into(),
log_level: None,
no_seccomp: false,
seccomp_filter: None,
http_api_max_payload_size: None,
mmds_size_limit: None,
socket_timeout_secs: None,
},
network: NetworkConfig {
bridge: "fcvmm0".into(),
cidr: "172.20.0.0/16".into(),
gateway: "172.20.0.1".into(),
dns: vec!["1.1.1.1".into(), "8.8.8.8".into()],
},
grpc: GrpcConfig {
unix_socket: "/run/firecracker-vmm/vmm.sock".into(),
tcp_addr: String::new(),
},
defaults: DefaultVmConfig {
vcpus: 1,
memory_mib: 512,
kernel: "/var/lib/firecracker-vmm/kernels/vmlinux".into(),
rootfs: "/var/lib/firecracker-vmm/images/ubuntu-22.04.ext4".into(),
boot_args: "console=ttyS0 reboot=k panic=1 pci=off".into(),
},
}
}
}
impl VmmConfig {
pub fn from_file(path: &str) -> Result<Self> {
let content = std::fs::read_to_string(path).map_err(|e| VmmError::Config(e.to_string()))?;
toml::from_str(&content).map_err(|e| VmmError::Config(e.to_string()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VmSpec {
pub name: String,
pub vcpus: u64,
pub memory_mib: u64,
pub kernel: String,
pub boot_args: String,
pub initrd: Option<String>,
pub smt: bool,
pub track_dirty_pages: bool,
pub huge_pages: Option<HugePagesSpec>,
pub cpu_template: Option<CpuTemplateSpec>,
pub rootfs: String,
pub root_readonly: bool,
#[serde(default)]
pub root_io_engine: IoEngine,
#[serde(default)]
pub root_cache_type: CacheType,
pub root_partuuid: Option<String>,
pub root_rate_limit: Option<RateLimitSpec>,
#[serde(default)]
pub extra_drives: Vec<DriveSpec>,
pub net_rx_rate_limit: Option<RateLimitSpec>,
pub net_tx_rate_limit: Option<RateLimitSpec>,
pub balloon: Option<BalloonSpec>,
pub vsock: Option<VsockSpec>,
#[serde(default)]
pub entropy_device: bool,
pub serial_out: Option<String>,
pub memory_hotplug: Option<MemoryHotplugSpec>,
pub mmds: Option<MmdsSpec>,
pub disk_size: Option<u64>,
pub ssh_public_key: Option<String>,
}
impl Default for VmSpec {
fn default() -> Self {
Self {
name: String::new(),
vcpus: 0,
memory_mib: 0,
kernel: String::new(),
boot_args: String::new(),
initrd: None,
smt: false,
track_dirty_pages: false,
huge_pages: None,
cpu_template: None,
rootfs: String::new(),
root_readonly: false,
root_io_engine: IoEngine::Sync,
root_cache_type: CacheType::Unsafe,
root_partuuid: None,
root_rate_limit: None,
extra_drives: Vec::new(),
net_rx_rate_limit: None,
net_tx_rate_limit: None,
balloon: None,
vsock: None,
entropy_device: false,
serial_out: None,
memory_hotplug: None,
mmds: None,
disk_size: None,
ssh_public_key: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SnapshotType {
Full,
Diff,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotRequest {
pub name: Option<String>,
pub snapshot_type: SnapshotType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RestoreSpec {
pub name: String,
pub snapshot_dir: String,
pub network_override: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config_has_sane_values() {
let cfg = VmmConfig::default();
assert_eq!(cfg.defaults.vcpus, 1);
assert_eq!(cfg.defaults.memory_mib, 512);
assert!(cfg.defaults.boot_args.contains("console=ttyS0"));
assert!(!cfg.network.cidr.is_empty());
assert!(!cfg.firecracker.binary.is_empty());
}
#[test]
fn test_vmm_config_json_roundtrip() {
let cfg = VmmConfig::default();
let json = serde_json::to_string(&cfg).unwrap();
let decoded: VmmConfig = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.defaults.vcpus, cfg.defaults.vcpus);
assert_eq!(decoded.defaults.memory_mib, cfg.defaults.memory_mib);
assert_eq!(decoded.network.cidr, cfg.network.cidr);
assert_eq!(decoded.network.gateway, cfg.network.gateway);
}
#[test]
fn test_vm_spec_default_is_empty() {
let spec = VmSpec::default();
assert!(spec.name.is_empty());
assert!(spec.kernel.is_empty());
assert!(spec.rootfs.is_empty());
assert_eq!(spec.vcpus, 0);
assert_eq!(spec.memory_mib, 0);
assert!(!spec.smt);
assert!(!spec.track_dirty_pages);
}
#[test]
fn test_from_file_missing_returns_config_error() {
let result = VmmConfig::from_file("/nonexistent/arcbox-test-config.toml");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
crate::error::VmmError::Config(_)
));
}
}