Skip to main content

arcbox_computer_runtime/config/
runtime.rs

1use serde::{Deserialize, Serialize};
2
3use crate::error::{Result, VmmError};
4
5use super::JailerConfig;
6
7/// Top-level configuration of the sandbox runtime, as the composer's
8/// config file spells it out (`/etc/arcbox/vmm.toml` in the System VM).
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct RuntimeConfig {
11    pub firecracker: FirecrackerConfig,
12    pub network: NetworkConfig,
13    pub grpc: GrpcConfig,
14    pub defaults: DefaultVmConfig,
15}
16
17/// The `[firecracker]` keys this runtime reads.
18///
19/// The section is named for the VMM because that is what a deployed
20/// `vmm.toml` has always called it, and the on-disk vocabulary is frozen
21/// (`computer/AGENTS.md`) — but nothing here is Firecracker's. The keys
22/// that configure a VMM adapter (its binaries, its process-level flags,
23/// the sandbox datapath) belong to whoever builds that adapter, and are
24/// read out of this same section by the composer; serde ignores what it
25/// does not know, so one section serves both halves and the file's shape
26/// is unchanged.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct FirecrackerConfig {
29    /// Jailer isolation for every sandbox VMM (absent = no isolation).
30    #[serde(default)]
31    pub jailer: Option<JailerConfig>,
32    /// Root data directory (VMs, snapshots, images).
33    pub data_dir: String,
34    /// Spare pre-warmed restore slots kept per snapshot id (CORE-78).
35    ///
36    /// A slot pre-executes the fixed host-side restore setup — jailer
37    /// chroot, Firecracker spawn, kernel/vmstate/mem staging, dm-snapshot
38    /// — so a restore only claims it and issues LoadSnapshot. Only
39    /// snapshots that have been restored at least once are pooled, for at
40    /// most two distinct snapshot ids (LRU-evicted). `0` disables pooling.
41    #[serde(default = "default_pool_size")]
42    pub pool_size: usize,
43    /// Serve eligible Creates from warm template snapshots (CORE-77).
44    ///
45    /// The first create of a template shape cold-boots and checkpoints the
46    /// idle guest; every later create of the same shape restores from that
47    /// snapshot instead of booting a kernel. Requires jailer isolation;
48    /// only networked creates without a custom boot recipe participate.
49    /// `false` restores plain cold boots for every create.
50    #[serde(default = "default_warm_create")]
51    pub warm_create: bool,
52    /// Where to look for the `dmsetup` binary that drives dm-snapshot CoW
53    /// rootfs images. The first entry that exists and answers
54    /// `dmsetup version` wins; none usable disables CoW (every sandbox
55    /// copies its rootfs). `PATH` is never searched. `None` — the field
56    /// absent from the file — leaves the choice to the composer, whose
57    /// fallback is the library's stock-distro list; an explicit list is
58    /// used exactly as written, so `[]` is how a config disables CoW.
59    #[serde(default)]
60    pub dmsetup_candidates: Option<Vec<String>>,
61}
62
63fn default_pool_size() -> usize {
64    1
65}
66
67fn default_warm_create() -> bool {
68    true
69}
70
71/// Network IP-pool settings for sandbox TAP interfaces.
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct NetworkConfig {
74    /// IP CIDR pool from which guest addresses are allocated.
75    pub cidr: String,
76    /// Default gateway advertised to guests.
77    pub gateway: String,
78    /// DNS servers advertised to guests.
79    pub dns: Vec<String>,
80}
81
82/// gRPC server transport configuration.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct GrpcConfig {
85    /// Unix-domain socket path (primary transport).
86    pub unix_socket: String,
87    /// Optional TCP address (`host:port`). Empty = disabled.
88    pub tcp_addr: String,
89}
90
91/// Default VM resource values used when a create request omits a field.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct DefaultVmConfig {
94    pub vcpus: u64,
95    pub memory_mib: u64,
96    pub kernel: String,
97    pub rootfs: String,
98    pub boot_args: String,
99}
100
101impl Default for RuntimeConfig {
102    fn default() -> Self {
103        Self {
104            firecracker: FirecrackerConfig {
105                jailer: None,
106                data_dir: "/var/lib/firecracker-vmm".into(),
107                pool_size: default_pool_size(),
108                warm_create: default_warm_create(),
109                dmsetup_candidates: None,
110            },
111            network: NetworkConfig {
112                cidr: "172.20.0.0/16".into(),
113                gateway: "172.20.0.1".into(),
114                dns: vec!["1.1.1.1".into(), "8.8.8.8".into()],
115            },
116            grpc: GrpcConfig {
117                unix_socket: "/run/firecracker-vmm/vmm.sock".into(),
118                tcp_addr: String::new(),
119            },
120            defaults: DefaultVmConfig {
121                vcpus: 1,
122                memory_mib: 512,
123                kernel: "/var/lib/firecracker-vmm/kernels/vmlinux".into(),
124                rootfs: "/var/lib/firecracker-vmm/images/ubuntu-22.04.ext4".into(),
125                boot_args: "console=ttyS0 reboot=k panic=1 pci=off".into(),
126            },
127        }
128    }
129}
130
131impl RuntimeConfig {
132    /// Load configuration from a TOML file.
133    pub fn from_file(path: &str) -> Result<Self> {
134        let content = std::fs::read_to_string(path).map_err(|e| VmmError::Config(e.to_string()))?;
135        toml::from_str(&content).map_err(|e| VmmError::Config(e.to_string()))
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_default_config_has_sane_values() {
145        let cfg = RuntimeConfig::default();
146        assert_eq!(cfg.defaults.vcpus, 1);
147        assert_eq!(cfg.defaults.memory_mib, 512);
148        assert!(cfg.defaults.boot_args.contains("console=ttyS0"));
149        assert_ne!(cfg.network.cidr, "");
150        assert_ne!(cfg.firecracker.data_dir, "");
151    }
152
153    #[test]
154    fn pool_size_defaults_to_one_spare_slot() {
155        assert_eq!(RuntimeConfig::default().firecracker.pool_size, 1);
156        // A config written before the knob existed still loads with the default.
157        let cfg: FirecrackerConfig = toml::from_str("data_dir = \"/var/lib/vmm\"\n").unwrap();
158        assert_eq!(cfg.pool_size, 1);
159    }
160
161    #[test]
162    fn warm_create_defaults_on_and_parses_the_escape_hatch() {
163        assert!(RuntimeConfig::default().firecracker.warm_create);
164        // A config written before the knob existed still loads with the default.
165        let cfg: FirecrackerConfig = toml::from_str("data_dir = \"/var/lib/vmm\"\n").unwrap();
166        assert!(cfg.warm_create);
167        // The escape hatch is reachable by config alone.
168        let cfg: FirecrackerConfig =
169            toml::from_str("data_dir = \"/var/lib/vmm\"\nwarm_create = false\n").unwrap();
170        assert!(!cfg.warm_create);
171    }
172
173    #[test]
174    fn dmsetup_candidates_absent_is_none_and_explicit_lists_parse_as_written() {
175        // A config written before the field existed still loads; the
176        // absence is preserved so the composer can supply its own list.
177        let cfg: FirecrackerConfig = toml::from_str("data_dir = \"/var/lib/vmm\"\n").unwrap();
178        assert_eq!(cfg.dmsetup_candidates, None);
179        // An explicit list is used exactly as written.
180        let cfg: FirecrackerConfig = toml::from_str(
181            "data_dir = \"/var/lib/vmm\"\n\
182             dmsetup_candidates = [\"/opt/arcbox/dmsetup\", \"/sbin/dmsetup\"]\n",
183        )
184        .unwrap();
185        assert_eq!(
186            cfg.dmsetup_candidates.as_deref(),
187            Some(
188                &[
189                    "/opt/arcbox/dmsetup".to_string(),
190                    "/sbin/dmsetup".to_string()
191                ][..]
192            )
193        );
194        // An empty list is a deliberate "no dmsetup" — CoW off.
195        let cfg: FirecrackerConfig = toml::from_str(
196            "data_dir = \"/var/lib/vmm\"\n\
197             dmsetup_candidates = []\n",
198        )
199        .unwrap();
200        assert_eq!(cfg.dmsetup_candidates.as_deref(), Some(&[][..]));
201    }
202
203    #[test]
204    fn test_vmm_config_json_roundtrip() {
205        let cfg = RuntimeConfig::default();
206        let json = serde_json::to_string(&cfg).unwrap();
207        let decoded: RuntimeConfig = serde_json::from_str(&json).unwrap();
208        assert_eq!(decoded.defaults.vcpus, cfg.defaults.vcpus);
209        assert_eq!(decoded.defaults.memory_mib, cfg.defaults.memory_mib);
210        assert_eq!(decoded.network.cidr, cfg.network.cidr);
211        assert_eq!(decoded.network.gateway, cfg.network.gateway);
212    }
213
214    #[test]
215    fn test_from_file_missing_returns_config_error() {
216        let result = RuntimeConfig::from_file("/nonexistent/arcbox-test-config.toml");
217        assert!(result.is_err());
218        assert!(matches!(
219            result.unwrap_err(),
220            crate::error::VmmError::Config(_)
221        ));
222    }
223}