1use std::net::Ipv4Addr;
13use std::path::PathBuf;
14
15use async_trait::async_trait;
16use serde::{Deserialize, Serialize};
17
18use crate::config::ResourceLimits;
19use crate::error::Result;
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct FsMount {
26 pub tag: String,
28 pub host_path: PathBuf,
30 pub read_only: bool,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct Entrypoint {
37 pub executable: String,
39 pub args: Vec<String>,
41 pub env: Vec<(String, String)>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct TeeInstanceConfig {
48 pub config_path: PathBuf,
50 pub tee_type: String,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct NetworkInstanceConfig {
57 pub passt_socket_path: PathBuf,
59
60 pub ip_address: Ipv4Addr,
62
63 pub gateway: Ipv4Addr,
65
66 pub prefix_len: u8,
68
69 pub mac_address: [u8; 6],
71
72 #[serde(default)]
74 pub dns_servers: Vec<Ipv4Addr>,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct InstanceSpec {
83 pub box_id: String,
85
86 pub vcpus: u8,
88
89 pub memory_mib: u32,
91
92 pub rootfs_path: PathBuf,
94
95 pub exec_socket_path: PathBuf,
97
98 #[serde(default)]
100 pub pty_socket_path: PathBuf,
101
102 #[serde(default)]
104 pub attest_socket_path: PathBuf,
105
106 pub fs_mounts: Vec<FsMount>,
108
109 pub entrypoint: Entrypoint,
111
112 pub console_output: Option<PathBuf>,
114
115 pub workdir: String,
117
118 pub tee_config: Option<TeeInstanceConfig>,
120
121 #[serde(default)]
123 pub port_map: Vec<String>,
124
125 #[serde(default)]
128 pub user: Option<String>,
129
130 #[serde(default)]
133 pub network: Option<NetworkInstanceConfig>,
134
135 #[serde(default)]
137 pub resource_limits: ResourceLimits,
138}
139
140impl Default for InstanceSpec {
141 fn default() -> Self {
142 Self {
143 box_id: String::new(),
144 vcpus: 2,
145 memory_mib: 512,
146 rootfs_path: PathBuf::new(),
147 exec_socket_path: PathBuf::new(),
148 pty_socket_path: PathBuf::new(),
149 attest_socket_path: PathBuf::new(),
150 fs_mounts: Vec::new(),
151 entrypoint: Entrypoint {
152 executable: String::new(),
153 args: Vec::new(),
154 env: Vec::new(),
155 },
156 console_output: None,
157 workdir: "/".to_string(),
158 tee_config: None,
159 port_map: Vec::new(),
160 user: None,
161 network: None,
162 resource_limits: ResourceLimits::default(),
163 }
164 }
165}
166
167#[derive(Debug, Clone, Default)]
171pub struct VmMetrics {
172 pub cpu_percent: Option<f32>,
174 pub memory_bytes: Option<u64>,
176}
177
178pub const DEFAULT_SHUTDOWN_TIMEOUT_MS: u64 = 10_000;
180
181pub fn parse_signal_name(name: &str) -> i32 {
186 let upper = name.trim().to_uppercase();
187 let short = upper.strip_prefix("SIG").unwrap_or(&upper);
188 match short {
189 "HUP" => 1,
190 "INT" => 2,
191 "QUIT" => 3,
192 "ILL" => 4,
193 "ABRT" => 6,
194 "FPE" => 8,
195 "KILL" => 9,
196 "USR1" => 10,
197 "SEGV" => 11,
198 "USR2" => 12,
199 "PIPE" => 13,
200 "ALRM" | "ALARM" => 14,
201 "TERM" => 15,
202 "CHLD" | "CLD" => 17,
203 "CONT" => 18,
204 "STOP" => 19,
205 "TSTP" => 20,
206 "WINCH" => 28,
207 _ => name.trim().parse::<i32>().unwrap_or(15),
208 }
209}
210
211pub trait VmHandler: Send + Sync {
216 fn stop(&mut self, signal: i32, timeout_ms: u64) -> Result<()>;
218
219 fn metrics(&self) -> VmMetrics;
221
222 fn is_running(&self) -> bool;
224
225 fn pid(&self) -> u32;
227
228 fn exit_code(&self) -> Option<i32> {
233 None
234 }
235}
236
237#[async_trait]
244pub trait VmmProvider: Send + Sync {
245 async fn start(&self, spec: &InstanceSpec) -> Result<Box<dyn VmHandler>>;
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252 use crate::config::ResourceLimits;
253
254 #[test]
255 fn test_parse_signal_name_term() {
256 assert_eq!(parse_signal_name("SIGTERM"), 15);
257 assert_eq!(parse_signal_name("TERM"), 15);
258 assert_eq!(parse_signal_name("15"), 15);
259 }
260
261 #[test]
262 fn test_parse_signal_name_variants() {
263 assert_eq!(parse_signal_name("SIGKILL"), 9);
264 assert_eq!(parse_signal_name("KILL"), 9);
265 assert_eq!(parse_signal_name("SIGHUP"), 1);
266 assert_eq!(parse_signal_name("SIGQUIT"), 3);
267 assert_eq!(parse_signal_name("SIGINT"), 2);
268 assert_eq!(parse_signal_name("SIGUSR1"), 10);
269 assert_eq!(parse_signal_name("SIGUSR2"), 12);
270 }
271
272 #[test]
273 fn test_parse_signal_name_numeric() {
274 assert_eq!(parse_signal_name("9"), 9);
275 assert_eq!(parse_signal_name("1"), 1);
276 }
277
278 #[test]
279 fn test_parse_signal_name_unknown_defaults_to_sigterm() {
280 assert_eq!(parse_signal_name("SIGFOO"), 15);
281 assert_eq!(parse_signal_name(""), 15);
282 assert_eq!(parse_signal_name("notasignal"), 15);
283 }
284
285 #[test]
286 fn test_parse_signal_name_case_insensitive() {
287 assert_eq!(parse_signal_name("sigterm"), 15);
288 assert_eq!(parse_signal_name("Sigterm"), 15);
289 }
290
291 #[test]
292 fn test_instance_spec_default_values() {
293 let spec = InstanceSpec::default();
294 assert_eq!(spec.vcpus, 2);
295 assert_eq!(spec.memory_mib, 512);
296 assert_eq!(spec.workdir, "/");
297 assert!(spec.box_id.is_empty());
298 assert!(spec.fs_mounts.is_empty());
299 assert!(spec.port_map.is_empty());
300 assert!(spec.tee_config.is_none());
301 assert!(spec.user.is_none());
302 assert!(spec.network.is_none());
303 assert!(spec.console_output.is_none());
304 }
305
306 #[test]
307 fn test_instance_spec_serde_roundtrip() {
308 let spec = InstanceSpec {
309 box_id: "test-box-123".to_string(),
310 vcpus: 4,
311 memory_mib: 2048,
312 rootfs_path: PathBuf::from("/tmp/rootfs"),
313 exec_socket_path: PathBuf::from("/tmp/exec.sock"),
314 pty_socket_path: PathBuf::from("/tmp/pty.sock"),
315 attest_socket_path: PathBuf::from("/tmp/attest.sock"),
316 fs_mounts: vec![FsMount {
317 tag: "workspace".to_string(),
318 host_path: PathBuf::from("/home/user/project"),
319 read_only: false,
320 }],
321 entrypoint: Entrypoint {
322 executable: "/usr/bin/agent".to_string(),
323 args: vec!["--port".to_string(), "8080".to_string()],
324 env: vec![("HOME".to_string(), "/root".to_string())],
325 },
326 console_output: Some(PathBuf::from("/tmp/console.log")),
327 workdir: "/app".to_string(),
328 tee_config: None,
329 port_map: vec!["8080:80".to_string()],
330 user: Some("1000:1000".to_string()),
331 network: None,
332 resource_limits: ResourceLimits::default(),
333 };
334
335 let json = serde_json::to_string(&spec).unwrap();
336 let deserialized: InstanceSpec = serde_json::from_str(&json).unwrap();
337
338 assert_eq!(deserialized.box_id, "test-box-123");
339 assert_eq!(deserialized.vcpus, 4);
340 assert_eq!(deserialized.memory_mib, 2048);
341 assert_eq!(deserialized.workdir, "/app");
342 assert_eq!(deserialized.fs_mounts.len(), 1);
343 assert_eq!(deserialized.fs_mounts[0].tag, "workspace");
344 assert!(!deserialized.fs_mounts[0].read_only);
345 assert_eq!(deserialized.entrypoint.executable, "/usr/bin/agent");
346 assert_eq!(deserialized.entrypoint.args.len(), 2);
347 assert_eq!(deserialized.entrypoint.env.len(), 1);
348 assert_eq!(deserialized.port_map, vec!["8080:80"]);
349 assert_eq!(deserialized.user, Some("1000:1000".to_string()));
350 }
351
352 #[test]
353 fn test_instance_spec_with_tee_config() {
354 let spec = InstanceSpec {
355 tee_config: Some(TeeInstanceConfig {
356 config_path: PathBuf::from("/etc/tee.json"),
357 tee_type: "snp".to_string(),
358 }),
359 ..Default::default()
360 };
361
362 let json = serde_json::to_string(&spec).unwrap();
363 let deserialized: InstanceSpec = serde_json::from_str(&json).unwrap();
364
365 let tee = deserialized.tee_config.unwrap();
366 assert_eq!(tee.tee_type, "snp");
367 assert_eq!(tee.config_path, PathBuf::from("/etc/tee.json"));
368 }
369
370 #[test]
371 fn test_instance_spec_with_network() {
372 let spec = InstanceSpec {
373 network: Some(NetworkInstanceConfig {
374 passt_socket_path: PathBuf::from("/tmp/passt.sock"),
375 ip_address: "10.0.0.2".parse().unwrap(),
376 gateway: "10.0.0.1".parse().unwrap(),
377 prefix_len: 24,
378 mac_address: [0x02, 0x42, 0xac, 0x11, 0x00, 0x02],
379 dns_servers: vec!["8.8.8.8".parse().unwrap()],
380 }),
381 ..Default::default()
382 };
383
384 let json = serde_json::to_string(&spec).unwrap();
385 let deserialized: InstanceSpec = serde_json::from_str(&json).unwrap();
386
387 let net = deserialized.network.unwrap();
388 assert_eq!(net.ip_address, "10.0.0.2".parse::<Ipv4Addr>().unwrap());
389 assert_eq!(net.gateway, "10.0.0.1".parse::<Ipv4Addr>().unwrap());
390 assert_eq!(net.prefix_len, 24);
391 assert_eq!(net.dns_servers.len(), 1);
392 }
393
394 #[test]
395 fn test_fs_mount_serde() {
396 let mount = FsMount {
397 tag: "data".to_string(),
398 host_path: PathBuf::from("/mnt/data"),
399 read_only: true,
400 };
401
402 let json = serde_json::to_string(&mount).unwrap();
403 let deserialized: FsMount = serde_json::from_str(&json).unwrap();
404
405 assert_eq!(deserialized.tag, "data");
406 assert_eq!(deserialized.host_path, PathBuf::from("/mnt/data"));
407 assert!(deserialized.read_only);
408 }
409
410 #[test]
411 fn test_entrypoint_serde() {
412 let ep = Entrypoint {
413 executable: "/bin/sh".to_string(),
414 args: vec!["-c".to_string(), "echo hello".to_string()],
415 env: vec![
416 ("PATH".to_string(), "/usr/bin".to_string()),
417 ("HOME".to_string(), "/root".to_string()),
418 ],
419 };
420
421 let json = serde_json::to_string(&ep).unwrap();
422 let deserialized: Entrypoint = serde_json::from_str(&json).unwrap();
423
424 assert_eq!(deserialized.executable, "/bin/sh");
425 assert_eq!(deserialized.args, vec!["-c", "echo hello"]);
426 assert_eq!(deserialized.env.len(), 2);
427 }
428
429 #[test]
430 fn test_instance_spec_deserialize_missing_optional_fields() {
431 let json = r#"{
432 "box_id": "min",
433 "vcpus": 1,
434 "memory_mib": 256,
435 "rootfs_path": "/rootfs",
436 "exec_socket_path": "/exec.sock",
437 "fs_mounts": [],
438 "entrypoint": {"executable": "/bin/sh", "args": [], "env": []},
439 "console_output": null,
440 "workdir": "/"
441 }"#;
442
443 let spec: InstanceSpec = serde_json::from_str(json).unwrap();
444 assert_eq!(spec.box_id, "min");
445 assert!(spec.port_map.is_empty());
446 assert!(spec.user.is_none());
447 assert!(spec.network.is_none());
448 assert!(spec.tee_config.is_none());
449 }
450
451 #[test]
452 fn test_resource_limits_in_spec() {
453 let spec = InstanceSpec {
454 resource_limits: ResourceLimits {
455 pids_limit: Some(100),
456 cpuset_cpus: Some("0-3".to_string()),
457 ..Default::default()
458 },
459 ..Default::default()
460 };
461
462 let json = serde_json::to_string(&spec).unwrap();
463 let deserialized: InstanceSpec = serde_json::from_str(&json).unwrap();
464
465 assert_eq!(deserialized.resource_limits.pids_limit, Some(100));
466 assert_eq!(
467 deserialized.resource_limits.cpuset_cpus,
468 Some("0-3".to_string())
469 );
470 }
471
472 #[test]
473 fn test_vm_metrics_default() {
474 let m = VmMetrics::default();
475 assert!(m.cpu_percent.is_none());
476 assert!(m.memory_bytes.is_none());
477 }
478
479 #[test]
480 fn test_vm_metrics_clone() {
481 let m = VmMetrics {
482 cpu_percent: Some(50.0),
483 memory_bytes: Some(1024 * 1024),
484 };
485 let cloned = m.clone();
486 assert_eq!(cloned.cpu_percent, Some(50.0));
487 assert_eq!(cloned.memory_bytes, Some(1024 * 1024));
488 }
489}