1use std::net::Ipv4Addr;
13#[cfg(target_os = "macos")]
14use std::os::fd::RawFd;
15use std::path::PathBuf;
16
17use async_trait::async_trait;
18use serde::{Deserialize, Serialize};
19
20use crate::config::ResourceLimits;
21use crate::error::Result;
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct FsMount {
28 pub tag: String,
30 pub host_path: PathBuf,
32 pub read_only: bool,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct Entrypoint {
39 pub executable: String,
41 pub args: Vec<String>,
43 pub env: Vec<(String, String)>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct TeeInstanceConfig {
50 pub config_path: PathBuf,
52 pub tee_type: String,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct NetworkInstanceConfig {
59 pub net_socket_path: PathBuf,
61
62 #[cfg(target_os = "macos")]
64 #[serde(default)]
65 pub net_socket_fd: Option<RawFd>,
66
67 #[cfg(target_os = "macos")]
69 #[serde(default)]
70 pub net_proxy_fd: Option<RawFd>,
71
72 pub ip_address: Ipv4Addr,
74
75 pub gateway: Ipv4Addr,
77
78 pub prefix_len: u8,
80
81 pub mac_address: [u8; 6],
83
84 #[serde(default)]
86 pub dns_servers: Vec<Ipv4Addr>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct InstanceSpec {
95 pub box_id: String,
97
98 pub vcpus: u8,
100
101 pub memory_mib: u32,
103
104 pub rootfs_path: PathBuf,
106
107 pub exec_socket_path: PathBuf,
109
110 #[serde(default)]
112 pub pty_socket_path: PathBuf,
113
114 #[serde(default)]
116 pub attest_socket_path: PathBuf,
117
118 #[serde(default)]
120 pub port_forward_socket_path: PathBuf,
121
122 pub fs_mounts: Vec<FsMount>,
124
125 pub entrypoint: Entrypoint,
127
128 #[serde(default)]
131 pub ksm: bool,
132
133 #[serde(default)]
137 pub snapshot_mem_file: Option<String>,
138
139 #[serde(default)]
142 pub snapshot_sock: Option<String>,
143
144 #[serde(default)]
150 pub restore_from: Option<String>,
151
152 pub console_output: Option<PathBuf>,
154
155 pub workdir: String,
157
158 pub tee_config: Option<TeeInstanceConfig>,
160
161 #[serde(default)]
163 pub port_map: Vec<String>,
164
165 #[serde(default)]
168 pub user: Option<String>,
169
170 #[serde(default)]
173 pub network: Option<NetworkInstanceConfig>,
174
175 #[serde(default)]
177 pub resource_limits: ResourceLimits,
178
179 #[serde(default)]
182 pub log_config: crate::log::LogConfig,
183}
184
185impl Default for InstanceSpec {
186 fn default() -> Self {
187 Self {
188 box_id: String::new(),
189 vcpus: 2,
190 memory_mib: 512,
191 rootfs_path: PathBuf::new(),
192 exec_socket_path: PathBuf::new(),
193 pty_socket_path: PathBuf::new(),
194 attest_socket_path: PathBuf::new(),
195 port_forward_socket_path: PathBuf::new(),
196 fs_mounts: Vec::new(),
197 entrypoint: Entrypoint {
198 executable: String::new(),
199 args: Vec::new(),
200 env: Vec::new(),
201 },
202 ksm: false,
203 snapshot_mem_file: None,
204 snapshot_sock: None,
205 restore_from: None,
206 console_output: None,
207 workdir: "/".to_string(),
208 tee_config: None,
209 port_map: Vec::new(),
210 user: None,
211 network: None,
212 resource_limits: ResourceLimits::default(),
213 log_config: crate::log::LogConfig::default(),
214 }
215 }
216}
217
218#[derive(Debug, Clone, Default)]
222pub struct VmMetrics {
223 pub cpu_percent: Option<f32>,
225 pub memory_bytes: Option<u64>,
227}
228
229pub const DEFAULT_SHUTDOWN_TIMEOUT_MS: u64 = 10_000;
231
232pub fn parse_signal_name(name: &str) -> i32 {
237 let upper = name.trim().to_uppercase();
238 let short = upper.strip_prefix("SIG").unwrap_or(&upper);
239 match short {
240 "HUP" => 1,
241 "INT" => 2,
242 "QUIT" => 3,
243 "ILL" => 4,
244 "ABRT" => 6,
245 "FPE" => 8,
246 "KILL" => 9,
247 "USR1" => 10,
248 "SEGV" => 11,
249 "USR2" => 12,
250 "PIPE" => 13,
251 "ALRM" | "ALARM" => 14,
252 "TERM" => 15,
253 "CHLD" | "CLD" => 17,
254 "CONT" => 18,
255 "STOP" => 19,
256 "TSTP" => 20,
257 "WINCH" => 28,
258 _ => name.trim().parse::<i32>().unwrap_or(15),
259 }
260}
261
262pub trait VmHandler: Send + Sync {
267 fn stop(&mut self, signal: i32, timeout_ms: u64) -> Result<()>;
269
270 fn metrics(&self) -> VmMetrics;
272
273 fn is_running(&self) -> bool;
275
276 fn has_exited(&self) -> bool {
286 #[cfg(target_os = "linux")]
287 {
288 linux_process_exited(self.pid())
289 }
290 #[cfg(not(target_os = "linux"))]
291 {
292 !self.is_running()
293 }
294 }
295
296 fn pid(&self) -> u32;
298
299 fn exit_code(&self) -> Option<i32> {
304 None
305 }
306
307 fn try_wait_exit(&mut self) -> Result<Option<i32>> {
313 Ok(None)
314 }
315}
316
317#[cfg(target_os = "linux")]
324pub(crate) fn linux_process_exited(pid: u32) -> bool {
325 match std::fs::read_to_string(format!("/proc/{pid}/stat")) {
326 Ok(stat) => match stat.rfind(')') {
327 Some(idx) => {
328 let state = stat[idx + 1..].trim_start().chars().next();
329 matches!(state, Some('Z') | Some('X'))
330 }
331 None => false,
333 },
334 Err(_) => true,
336 }
337}
338
339#[async_trait]
346pub trait VmmProvider: Send + Sync {
347 async fn start(&self, spec: &InstanceSpec) -> Result<Box<dyn VmHandler>>;
349}
350
351#[cfg(test)]
352mod tests {
353 use super::*;
354 use crate::config::ResourceLimits;
355
356 #[cfg(target_os = "linux")]
357 #[test]
358 fn test_linux_process_exited_current_process_is_alive() {
359 assert!(!linux_process_exited(std::process::id()));
361 }
362
363 #[cfg(target_os = "linux")]
364 #[test]
365 fn test_linux_process_exited_missing_pid_is_exited() {
366 assert!(linux_process_exited(0x7fff_fffe));
368 }
369
370 #[test]
371 fn test_parse_signal_name_term() {
372 assert_eq!(parse_signal_name("SIGTERM"), 15);
373 assert_eq!(parse_signal_name("TERM"), 15);
374 assert_eq!(parse_signal_name("15"), 15);
375 }
376
377 #[test]
378 fn test_parse_signal_name_variants() {
379 assert_eq!(parse_signal_name("SIGKILL"), 9);
380 assert_eq!(parse_signal_name("KILL"), 9);
381 assert_eq!(parse_signal_name("SIGHUP"), 1);
382 assert_eq!(parse_signal_name("SIGQUIT"), 3);
383 assert_eq!(parse_signal_name("SIGINT"), 2);
384 assert_eq!(parse_signal_name("SIGUSR1"), 10);
385 assert_eq!(parse_signal_name("SIGUSR2"), 12);
386 }
387
388 #[test]
389 fn test_parse_signal_name_numeric() {
390 assert_eq!(parse_signal_name("9"), 9);
391 assert_eq!(parse_signal_name("1"), 1);
392 }
393
394 #[test]
395 fn test_parse_signal_name_unknown_defaults_to_sigterm() {
396 assert_eq!(parse_signal_name("SIGFOO"), 15);
397 assert_eq!(parse_signal_name(""), 15);
398 assert_eq!(parse_signal_name("notasignal"), 15);
399 }
400
401 #[test]
402 fn test_parse_signal_name_case_insensitive() {
403 assert_eq!(parse_signal_name("sigterm"), 15);
404 assert_eq!(parse_signal_name("Sigterm"), 15);
405 }
406
407 #[test]
408 fn test_instance_spec_default_values() {
409 let spec = InstanceSpec::default();
410 assert_eq!(spec.vcpus, 2);
411 assert_eq!(spec.memory_mib, 512);
412 assert_eq!(spec.workdir, "/");
413 assert!(spec.box_id.is_empty());
414 assert!(spec.fs_mounts.is_empty());
415 assert!(spec.port_map.is_empty());
416 assert!(spec.tee_config.is_none());
417 assert!(spec.user.is_none());
418 assert!(spec.network.is_none());
419 assert!(spec.console_output.is_none());
420 }
421
422 #[test]
423 fn test_instance_spec_serde_roundtrip() {
424 let spec = InstanceSpec {
425 box_id: "test-box-123".to_string(),
426 ksm: false,
427 snapshot_mem_file: None,
428 snapshot_sock: None,
429 restore_from: None,
430 vcpus: 4,
431 memory_mib: 2048,
432 rootfs_path: PathBuf::from("/tmp/rootfs"),
433 exec_socket_path: PathBuf::from("/tmp/exec.sock"),
434 pty_socket_path: PathBuf::from("/tmp/pty.sock"),
435 attest_socket_path: PathBuf::from("/tmp/attest.sock"),
436 port_forward_socket_path: PathBuf::from("/tmp/portfwd.sock"),
437 fs_mounts: vec![FsMount {
438 tag: "workspace".to_string(),
439 host_path: PathBuf::from("/home/user/project"),
440 read_only: false,
441 }],
442 entrypoint: Entrypoint {
443 executable: "/usr/bin/agent".to_string(),
444 args: vec!["--port".to_string(), "8080".to_string()],
445 env: vec![("HOME".to_string(), "/root".to_string())],
446 },
447 console_output: Some(PathBuf::from("/tmp/console.log")),
448 workdir: "/app".to_string(),
449 tee_config: None,
450 port_map: vec!["8080:80".to_string()],
451 user: Some("1000:1000".to_string()),
452 network: None,
453 resource_limits: ResourceLimits::default(),
454 log_config: crate::log::LogConfig::default(),
455 };
456
457 let json = serde_json::to_string(&spec).unwrap();
458 let deserialized: InstanceSpec = serde_json::from_str(&json).unwrap();
459
460 assert_eq!(deserialized.box_id, "test-box-123");
461 assert_eq!(deserialized.vcpus, 4);
462 assert_eq!(deserialized.memory_mib, 2048);
463 assert_eq!(deserialized.workdir, "/app");
464 assert_eq!(deserialized.fs_mounts.len(), 1);
465 assert_eq!(deserialized.fs_mounts[0].tag, "workspace");
466 assert!(!deserialized.fs_mounts[0].read_only);
467 assert_eq!(deserialized.entrypoint.executable, "/usr/bin/agent");
468 assert_eq!(deserialized.entrypoint.args.len(), 2);
469 assert_eq!(deserialized.entrypoint.env.len(), 1);
470 assert_eq!(
471 deserialized.port_forward_socket_path,
472 PathBuf::from("/tmp/portfwd.sock")
473 );
474 assert_eq!(deserialized.port_map, vec!["8080:80"]);
475 assert_eq!(deserialized.user, Some("1000:1000".to_string()));
476 }
477
478 #[test]
479 fn test_instance_spec_with_tee_config() {
480 let spec = InstanceSpec {
481 tee_config: Some(TeeInstanceConfig {
482 config_path: PathBuf::from("/etc/tee.json"),
483 tee_type: "snp".to_string(),
484 }),
485 ..Default::default()
486 };
487
488 let json = serde_json::to_string(&spec).unwrap();
489 let deserialized: InstanceSpec = serde_json::from_str(&json).unwrap();
490
491 let tee = deserialized.tee_config.unwrap();
492 assert_eq!(tee.tee_type, "snp");
493 assert_eq!(tee.config_path, PathBuf::from("/etc/tee.json"));
494 }
495
496 #[test]
497 fn test_instance_spec_with_network() {
498 let spec = InstanceSpec {
499 network: Some(NetworkInstanceConfig {
500 net_socket_path: PathBuf::from("/tmp/net.sock"),
501 #[cfg(target_os = "macos")]
502 net_socket_fd: Some(42),
503 #[cfg(target_os = "macos")]
504 net_proxy_fd: Some(43),
505 ip_address: "10.0.0.2".parse().unwrap(),
506 gateway: "10.0.0.1".parse().unwrap(),
507 prefix_len: 24,
508 mac_address: [0x02, 0x42, 0xac, 0x11, 0x00, 0x02],
509 dns_servers: vec!["8.8.8.8".parse().unwrap()],
510 }),
511 ..Default::default()
512 };
513
514 let json = serde_json::to_string(&spec).unwrap();
515 let deserialized: InstanceSpec = serde_json::from_str(&json).unwrap();
516
517 let net = deserialized.network.unwrap();
518 #[cfg(target_os = "macos")]
519 assert_eq!(net.net_socket_fd, Some(42));
520 #[cfg(target_os = "macos")]
521 assert_eq!(net.net_proxy_fd, Some(43));
522 assert_eq!(net.ip_address, "10.0.0.2".parse::<Ipv4Addr>().unwrap());
523 assert_eq!(net.gateway, "10.0.0.1".parse::<Ipv4Addr>().unwrap());
524 assert_eq!(net.prefix_len, 24);
525 assert_eq!(net.dns_servers.len(), 1);
526 }
527
528 #[test]
529 fn test_fs_mount_serde() {
530 let mount = FsMount {
531 tag: "data".to_string(),
532 host_path: PathBuf::from("/mnt/data"),
533 read_only: true,
534 };
535
536 let json = serde_json::to_string(&mount).unwrap();
537 let deserialized: FsMount = serde_json::from_str(&json).unwrap();
538
539 assert_eq!(deserialized.tag, "data");
540 assert_eq!(deserialized.host_path, PathBuf::from("/mnt/data"));
541 assert!(deserialized.read_only);
542 }
543
544 #[test]
545 fn test_entrypoint_serde() {
546 let ep = Entrypoint {
547 executable: "/bin/sh".to_string(),
548 args: vec!["-c".to_string(), "echo hello".to_string()],
549 env: vec![
550 ("PATH".to_string(), "/usr/bin".to_string()),
551 ("HOME".to_string(), "/root".to_string()),
552 ],
553 };
554
555 let json = serde_json::to_string(&ep).unwrap();
556 let deserialized: Entrypoint = serde_json::from_str(&json).unwrap();
557
558 assert_eq!(deserialized.executable, "/bin/sh");
559 assert_eq!(deserialized.args, vec!["-c", "echo hello"]);
560 assert_eq!(deserialized.env.len(), 2);
561 }
562
563 #[test]
564 fn test_instance_spec_deserialize_missing_optional_fields() {
565 let json = r#"{
566 "box_id": "min",
567 "vcpus": 1,
568 "memory_mib": 256,
569 "rootfs_path": "/rootfs",
570 "exec_socket_path": "/exec.sock",
571 "fs_mounts": [],
572 "entrypoint": {"executable": "/bin/sh", "args": [], "env": []},
573 "console_output": null,
574 "workdir": "/"
575 }"#;
576
577 let spec: InstanceSpec = serde_json::from_str(json).unwrap();
578 assert_eq!(spec.box_id, "min");
579 assert!(spec.port_map.is_empty());
580 assert!(spec.user.is_none());
581 assert!(spec.network.is_none());
582 assert!(spec.tee_config.is_none());
583 }
584
585 #[test]
586 fn test_resource_limits_in_spec() {
587 let spec = InstanceSpec {
588 resource_limits: ResourceLimits {
589 pids_limit: Some(100),
590 cpuset_cpus: Some("0-3".to_string()),
591 ..Default::default()
592 },
593 ..Default::default()
594 };
595
596 let json = serde_json::to_string(&spec).unwrap();
597 let deserialized: InstanceSpec = serde_json::from_str(&json).unwrap();
598
599 assert_eq!(deserialized.resource_limits.pids_limit, Some(100));
600 assert_eq!(
601 deserialized.resource_limits.cpuset_cpus,
602 Some("0-3".to_string())
603 );
604 }
605
606 #[test]
607 fn test_vm_metrics_default() {
608 let m = VmMetrics::default();
609 assert!(m.cpu_percent.is_none());
610 assert!(m.memory_bytes.is_none());
611 }
612
613 #[test]
614 fn test_vm_metrics_clone() {
615 let m = VmMetrics {
616 cpu_percent: Some(50.0),
617 memory_bytes: Some(1024 * 1024),
618 };
619 let cloned = m.clone();
620 assert_eq!(cloned.cpu_percent, Some(50.0));
621 assert_eq!(cloned.memory_bytes, Some(1024 * 1024));
622 }
623}