arcbox_computer_runtime/config/
runtime.rs1use serde::{Deserialize, Serialize};
2
3use crate::error::{ComputerError, Result};
4
5use super::JailerConfig;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct RuntimeConfig {
11 pub firecracker: ComputerConfig,
12 pub network: NetworkConfig,
13 pub grpc: GrpcConfig,
14 pub defaults: DefaultVmConfig,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ComputerConfig {
29 #[serde(default)]
31 pub jailer: Option<JailerConfig>,
32 pub data_dir: String,
34 #[serde(default = "default_pool_size")]
42 pub pool_size: usize,
43 #[serde(default = "default_warm_create")]
51 pub warm_create: bool,
52 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct NetworkConfig {
74 pub cidr: String,
76 pub gateway: String,
78 pub dns: Vec<String>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct GrpcConfig {
85 pub unix_socket: String,
87 pub tcp_addr: String,
89}
90
91#[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,
101}
102
103impl Default for RuntimeConfig {
104 fn default() -> Self {
105 Self {
106 firecracker: ComputerConfig {
107 jailer: None,
108 data_dir: "/var/lib/firecracker-vmm".into(),
109 pool_size: default_pool_size(),
110 warm_create: default_warm_create(),
111 dmsetup_candidates: None,
112 },
113 network: NetworkConfig {
114 cidr: "172.20.0.0/16".into(),
115 gateway: "172.20.0.1".into(),
116 dns: vec!["1.1.1.1".into(), "8.8.8.8".into()],
117 },
118 grpc: GrpcConfig {
119 unix_socket: "/run/firecracker-vmm/vmm.sock".into(),
120 tcp_addr: String::new(),
121 },
122 defaults: DefaultVmConfig {
123 vcpus: 1,
124 memory_mib: 512,
125 kernel: "/var/lib/firecracker-vmm/kernels/vmlinux".into(),
126 rootfs: "/var/lib/firecracker-vmm/images/ubuntu-22.04.ext4".into(),
127 boot_args: format!(
128 "console=ttyS0 reboot=k panic=1 pci=off init={}",
129 crate::VM_AGENT_PATH
130 ),
131 },
132 }
133 }
134}
135
136impl RuntimeConfig {
137 pub fn from_file(path: &str) -> Result<Self> {
139 let content =
140 std::fs::read_to_string(path).map_err(|e| ComputerError::Config(e.to_string()))?;
141 toml::from_str(&content).map_err(|e| ComputerError::Config(e.to_string()))
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_default_config_has_sane_values() {
151 let cfg = RuntimeConfig::default();
152 assert_eq!(cfg.defaults.vcpus, 1);
153 assert_eq!(cfg.defaults.memory_mib, 512);
154 assert!(cfg.defaults.boot_args.contains("console=ttyS0"));
155 assert!(
156 cfg.defaults
157 .boot_args
158 .split_whitespace()
159 .any(|arg| arg == format!("init={}", crate::VM_AGENT_PATH)),
160 "the default command line must select vm-agent as PID 1"
161 );
162 assert_ne!(cfg.network.cidr, "");
163 assert_ne!(cfg.firecracker.data_dir, "");
164 }
165
166 #[test]
167 fn pool_size_defaults_to_one_spare_slot() {
168 assert_eq!(RuntimeConfig::default().firecracker.pool_size, 1);
169 let cfg: ComputerConfig = toml::from_str("data_dir = \"/var/lib/vmm\"\n").unwrap();
171 assert_eq!(cfg.pool_size, 1);
172 }
173
174 #[test]
175 fn warm_create_defaults_on_and_parses_the_escape_hatch() {
176 assert!(RuntimeConfig::default().firecracker.warm_create);
177 let cfg: ComputerConfig = toml::from_str("data_dir = \"/var/lib/vmm\"\n").unwrap();
179 assert!(cfg.warm_create);
180 let cfg: ComputerConfig =
182 toml::from_str("data_dir = \"/var/lib/vmm\"\nwarm_create = false\n").unwrap();
183 assert!(!cfg.warm_create);
184 }
185
186 #[test]
187 fn dmsetup_candidates_absent_is_none_and_explicit_lists_parse_as_written() {
188 let cfg: ComputerConfig = toml::from_str("data_dir = \"/var/lib/vmm\"\n").unwrap();
191 assert_eq!(cfg.dmsetup_candidates, None);
192 let cfg: ComputerConfig = toml::from_str(
194 "data_dir = \"/var/lib/vmm\"\n\
195 dmsetup_candidates = [\"/opt/arcbox/dmsetup\", \"/sbin/dmsetup\"]\n",
196 )
197 .unwrap();
198 assert_eq!(
199 cfg.dmsetup_candidates.as_deref(),
200 Some(
201 &[
202 "/opt/arcbox/dmsetup".to_string(),
203 "/sbin/dmsetup".to_string()
204 ][..]
205 )
206 );
207 let cfg: ComputerConfig = toml::from_str(
209 "data_dir = \"/var/lib/vmm\"\n\
210 dmsetup_candidates = []\n",
211 )
212 .unwrap();
213 assert_eq!(cfg.dmsetup_candidates.as_deref(), Some(&[][..]));
214 }
215
216 #[test]
217 fn test_vmm_config_json_roundtrip() {
218 let cfg = RuntimeConfig::default();
219 let json = serde_json::to_string(&cfg).unwrap();
220 let decoded: RuntimeConfig = serde_json::from_str(&json).unwrap();
221 assert_eq!(decoded.defaults.vcpus, cfg.defaults.vcpus);
222 assert_eq!(decoded.defaults.memory_mib, cfg.defaults.memory_mib);
223 assert_eq!(decoded.network.cidr, cfg.network.cidr);
224 assert_eq!(decoded.network.gateway, cfg.network.gateway);
225 }
226
227 #[test]
228 fn test_from_file_missing_returns_config_error() {
229 let result = RuntimeConfig::from_file("/nonexistent/arcbox-test-config.toml");
230 assert!(result.is_err());
231 assert!(matches!(
232 result.unwrap_err(),
233 crate::error::ComputerError::Config(_)
234 ));
235 }
236}