1use std::net::Ipv4Addr;
13#[cfg(unix)]
14use std::os::fd::RawFd;
15use std::path::PathBuf;
16
17use async_trait::async_trait;
18use serde::{Deserialize, Serialize};
19
20use crate::config::{ResourceLimits, DEFAULT_VCPUS};
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 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub net_stats_path: Option<PathBuf>,
65
66 #[cfg(unix)]
68 #[serde(default)]
69 pub net_socket_fd: Option<RawFd>,
70
71 #[cfg(unix)]
73 #[serde(default)]
74 pub net_proxy_fd: Option<RawFd>,
75
76 #[cfg(unix)]
78 #[serde(default, skip_serializing_if = "Option::is_none")]
79 pub bridge_socket_dir: Option<PathBuf>,
80
81 pub ip_address: Ipv4Addr,
83
84 pub gateway: Ipv4Addr,
86
87 pub prefix_len: u8,
89
90 pub mac_address: [u8; 6],
92
93 #[serde(default)]
95 pub dns_servers: Vec<Ipv4Addr>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct InstanceSpec {
104 pub box_id: String,
106
107 pub vcpus: u8,
109
110 pub memory_mib: u32,
112
113 pub rootfs_path: PathBuf,
115
116 pub exec_socket_path: PathBuf,
118
119 #[serde(default)]
121 pub pty_socket_path: PathBuf,
122
123 #[serde(default)]
125 pub attest_socket_path: PathBuf,
126
127 #[serde(default)]
129 pub port_forward_socket_path: PathBuf,
130
131 pub fs_mounts: Vec<FsMount>,
133
134 pub entrypoint: Entrypoint,
136
137 #[serde(default)]
140 pub ksm: bool,
141
142 #[serde(default)]
146 pub snapshot_mem_file: Option<String>,
147
148 #[serde(default)]
151 pub snapshot_sock: Option<String>,
152
153 #[serde(default)]
159 pub restore_from: Option<String>,
160
161 pub console_output: Option<PathBuf>,
163
164 pub workdir: String,
166
167 pub tee_config: Option<TeeInstanceConfig>,
169
170 #[serde(default)]
172 pub port_map: Vec<String>,
173
174 #[serde(default)]
177 pub user: Option<String>,
178
179 #[serde(default)]
182 pub network: Option<NetworkInstanceConfig>,
183
184 #[serde(default)]
186 pub disable_tsi: bool,
187
188 #[serde(default)]
190 pub resource_limits: ResourceLimits,
191
192 #[serde(default)]
195 pub log_config: crate::log::LogConfig,
196}
197
198impl Default for InstanceSpec {
199 fn default() -> Self {
200 Self {
201 box_id: String::new(),
202 vcpus: DEFAULT_VCPUS as u8,
203 memory_mib: 512,
204 rootfs_path: PathBuf::new(),
205 exec_socket_path: PathBuf::new(),
206 pty_socket_path: PathBuf::new(),
207 attest_socket_path: PathBuf::new(),
208 port_forward_socket_path: PathBuf::new(),
209 fs_mounts: Vec::new(),
210 entrypoint: Entrypoint {
211 executable: String::new(),
212 args: Vec::new(),
213 env: Vec::new(),
214 },
215 ksm: false,
216 snapshot_mem_file: None,
217 snapshot_sock: None,
218 restore_from: None,
219 console_output: None,
220 workdir: "/".to_string(),
221 tee_config: None,
222 port_map: Vec::new(),
223 user: None,
224 network: None,
225 disable_tsi: false,
226 resource_limits: ResourceLimits::default(),
227 log_config: crate::log::LogConfig::default(),
228 }
229 }
230}
231
232#[derive(Debug, Clone, Default)]
236pub struct VmMetrics {
237 pub cpu_percent: Option<f32>,
239 pub memory_bytes: Option<u64>,
241}
242
243pub const DEFAULT_SHUTDOWN_TIMEOUT_MS: u64 = 10_000;
245
246pub fn parse_signal_name(name: &str) -> i32 {
251 let upper = name.trim().to_uppercase();
252 let short = upper.strip_prefix("SIG").unwrap_or(&upper);
253 match short {
254 "HUP" => 1,
255 "INT" => 2,
256 "QUIT" => 3,
257 "ILL" => 4,
258 "ABRT" => 6,
259 "FPE" => 8,
260 "KILL" => 9,
261 "USR1" => 10,
262 "SEGV" => 11,
263 "USR2" => 12,
264 "PIPE" => 13,
265 "ALRM" | "ALARM" => 14,
266 "TERM" => 15,
267 "CHLD" | "CLD" => 17,
268 "CONT" => 18,
269 "STOP" => 19,
270 "TSTP" => 20,
271 "WINCH" => 28,
272 _ => name.trim().parse::<i32>().unwrap_or(15),
273 }
274}
275
276pub trait VmHandler: Send + Sync {
281 fn stop(&mut self, signal: i32, timeout_ms: u64) -> Result<()>;
283
284 fn metrics(&self) -> VmMetrics;
286
287 fn is_running(&self) -> bool;
289
290 fn has_exited(&self) -> bool {
300 #[cfg(target_os = "linux")]
301 {
302 linux_process_exited(self.pid())
303 }
304 #[cfg(not(target_os = "linux"))]
305 {
306 !self.is_running()
307 }
308 }
309
310 fn pid(&self) -> u32;
312
313 fn exit_code(&self) -> Option<i32> {
318 None
319 }
320
321 fn try_wait_exit(&mut self) -> Result<Option<i32>> {
327 Ok(None)
328 }
329}
330
331#[cfg(target_os = "linux")]
338pub(crate) fn linux_process_exited(pid: u32) -> bool {
339 match std::fs::read_to_string(format!("/proc/{pid}/stat")) {
340 Ok(stat) => match stat.rfind(')') {
341 Some(idx) => {
342 let state = stat[idx + 1..].trim_start().chars().next();
343 matches!(state, Some('Z') | Some('X'))
344 }
345 None => false,
347 },
348 Err(_) => true,
350 }
351}
352
353#[async_trait]
360pub trait VmmProvider: Send + Sync {
361 async fn start(&self, spec: &InstanceSpec) -> Result<Box<dyn VmHandler>>;
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use crate::config::ResourceLimits;
369
370 #[cfg(target_os = "linux")]
371 #[test]
372 fn test_linux_process_exited_current_process_is_alive() {
373 assert!(!linux_process_exited(std::process::id()));
375 }
376
377 #[cfg(target_os = "linux")]
378 #[test]
379 fn test_linux_process_exited_missing_pid_is_exited() {
380 assert!(linux_process_exited(0x7fff_fffe));
382 }
383
384 #[test]
385 fn test_parse_signal_name_term() {
386 assert_eq!(parse_signal_name("SIGTERM"), 15);
387 assert_eq!(parse_signal_name("TERM"), 15);
388 assert_eq!(parse_signal_name("15"), 15);
389 }
390
391 #[test]
392 fn test_parse_signal_name_variants() {
393 assert_eq!(parse_signal_name("SIGKILL"), 9);
394 assert_eq!(parse_signal_name("KILL"), 9);
395 assert_eq!(parse_signal_name("SIGHUP"), 1);
396 assert_eq!(parse_signal_name("SIGQUIT"), 3);
397 assert_eq!(parse_signal_name("SIGINT"), 2);
398 assert_eq!(parse_signal_name("SIGUSR1"), 10);
399 assert_eq!(parse_signal_name("SIGUSR2"), 12);
400 }
401
402 #[test]
403 fn test_parse_signal_name_numeric() {
404 assert_eq!(parse_signal_name("9"), 9);
405 assert_eq!(parse_signal_name("1"), 1);
406 }
407
408 #[test]
409 fn test_parse_signal_name_unknown_defaults_to_sigterm() {
410 assert_eq!(parse_signal_name("SIGFOO"), 15);
411 assert_eq!(parse_signal_name(""), 15);
412 assert_eq!(parse_signal_name("notasignal"), 15);
413 }
414
415 #[test]
416 fn test_parse_signal_name_case_insensitive() {
417 assert_eq!(parse_signal_name("sigterm"), 15);
418 assert_eq!(parse_signal_name("Sigterm"), 15);
419 }
420
421 #[test]
422 fn test_instance_spec_default_values() {
423 let spec = InstanceSpec::default();
424 assert_eq!(spec.vcpus, DEFAULT_VCPUS as u8);
425 assert_eq!(spec.memory_mib, 512);
426 assert_eq!(spec.workdir, "/");
427 assert!(spec.box_id.is_empty());
428 assert!(spec.fs_mounts.is_empty());
429 assert!(spec.port_map.is_empty());
430 assert!(spec.tee_config.is_none());
431 assert!(spec.user.is_none());
432 assert!(spec.network.is_none());
433 assert!(!spec.disable_tsi);
434 assert!(spec.console_output.is_none());
435 }
436
437 #[test]
438 fn test_instance_spec_missing_disable_tsi_keeps_legacy_default() {
439 let mut value = serde_json::to_value(InstanceSpec::default()).unwrap();
440 value
441 .as_object_mut()
442 .unwrap()
443 .remove("disable_tsi")
444 .unwrap();
445
446 let spec: InstanceSpec = serde_json::from_value(value).unwrap();
447
448 assert!(!spec.disable_tsi);
449 }
450
451 #[test]
452 fn test_instance_spec_serde_roundtrip() {
453 let spec = InstanceSpec {
454 box_id: "test-box-123".to_string(),
455 ksm: false,
456 snapshot_mem_file: None,
457 snapshot_sock: None,
458 restore_from: None,
459 vcpus: 4,
460 memory_mib: 2048,
461 rootfs_path: PathBuf::from("/tmp/rootfs"),
462 exec_socket_path: PathBuf::from("/tmp/exec.sock"),
463 pty_socket_path: PathBuf::from("/tmp/pty.sock"),
464 attest_socket_path: PathBuf::from("/tmp/attest.sock"),
465 port_forward_socket_path: PathBuf::from("/tmp/portfwd.sock"),
466 fs_mounts: vec![FsMount {
467 tag: "workspace".to_string(),
468 host_path: PathBuf::from("/home/user/project"),
469 read_only: false,
470 }],
471 entrypoint: Entrypoint {
472 executable: "/usr/bin/agent".to_string(),
473 args: vec!["--port".to_string(), "8080".to_string()],
474 env: vec![("HOME".to_string(), "/root".to_string())],
475 },
476 console_output: Some(PathBuf::from("/tmp/console.log")),
477 workdir: "/app".to_string(),
478 tee_config: None,
479 port_map: vec!["8080:80".to_string()],
480 user: Some("1000:1000".to_string()),
481 network: None,
482 disable_tsi: true,
483 resource_limits: ResourceLimits::default(),
484 log_config: crate::log::LogConfig::default(),
485 };
486
487 let json = serde_json::to_string(&spec).unwrap();
488 let deserialized: InstanceSpec = serde_json::from_str(&json).unwrap();
489
490 assert_eq!(deserialized.box_id, "test-box-123");
491 assert_eq!(deserialized.vcpus, 4);
492 assert_eq!(deserialized.memory_mib, 2048);
493 assert_eq!(deserialized.workdir, "/app");
494 assert_eq!(deserialized.fs_mounts.len(), 1);
495 assert_eq!(deserialized.fs_mounts[0].tag, "workspace");
496 assert!(!deserialized.fs_mounts[0].read_only);
497 assert_eq!(deserialized.entrypoint.executable, "/usr/bin/agent");
498 assert_eq!(deserialized.entrypoint.args.len(), 2);
499 assert_eq!(deserialized.entrypoint.env.len(), 1);
500 assert_eq!(
501 deserialized.port_forward_socket_path,
502 PathBuf::from("/tmp/portfwd.sock")
503 );
504 assert_eq!(deserialized.port_map, vec!["8080:80"]);
505 assert_eq!(deserialized.user, Some("1000:1000".to_string()));
506 assert!(deserialized.disable_tsi);
507 }
508
509 #[test]
510 fn test_instance_spec_with_tee_config() {
511 let spec = InstanceSpec {
512 tee_config: Some(TeeInstanceConfig {
513 config_path: PathBuf::from("/etc/tee.json"),
514 tee_type: "snp".to_string(),
515 }),
516 ..Default::default()
517 };
518
519 let json = serde_json::to_string(&spec).unwrap();
520 let deserialized: InstanceSpec = serde_json::from_str(&json).unwrap();
521
522 let tee = deserialized.tee_config.unwrap();
523 assert_eq!(tee.tee_type, "snp");
524 assert_eq!(tee.config_path, PathBuf::from("/etc/tee.json"));
525 }
526
527 #[test]
528 fn test_instance_spec_with_network() {
529 let spec = InstanceSpec {
530 network: Some(NetworkInstanceConfig {
531 net_socket_path: PathBuf::from("/tmp/net.sock"),
532 net_stats_path: Some(PathBuf::from("/tmp/net.stats.json")),
533 #[cfg(unix)]
534 net_socket_fd: Some(42),
535 #[cfg(unix)]
536 net_proxy_fd: Some(43),
537 #[cfg(unix)]
538 bridge_socket_dir: Some(PathBuf::from("/tmp/a3s-switch")),
539 ip_address: "10.0.0.2".parse().unwrap(),
540 gateway: "10.0.0.1".parse().unwrap(),
541 prefix_len: 24,
542 mac_address: [0x02, 0x42, 0xac, 0x11, 0x00, 0x02],
543 dns_servers: vec!["8.8.8.8".parse().unwrap()],
544 }),
545 ..Default::default()
546 };
547
548 let json = serde_json::to_string(&spec).unwrap();
549 let deserialized: InstanceSpec = serde_json::from_str(&json).unwrap();
550
551 let net = deserialized.network.unwrap();
552 assert_eq!(
553 net.net_stats_path,
554 Some(PathBuf::from("/tmp/net.stats.json"))
555 );
556 #[cfg(unix)]
557 assert_eq!(net.net_socket_fd, Some(42));
558 #[cfg(unix)]
559 assert_eq!(net.net_proxy_fd, Some(43));
560 assert_eq!(net.ip_address, "10.0.0.2".parse::<Ipv4Addr>().unwrap());
561 assert_eq!(net.gateway, "10.0.0.1".parse::<Ipv4Addr>().unwrap());
562 assert_eq!(net.prefix_len, 24);
563 assert_eq!(net.dns_servers.len(), 1);
564 }
565
566 #[test]
567 fn test_fs_mount_serde() {
568 let mount = FsMount {
569 tag: "data".to_string(),
570 host_path: PathBuf::from("/mnt/data"),
571 read_only: true,
572 };
573
574 let json = serde_json::to_string(&mount).unwrap();
575 let deserialized: FsMount = serde_json::from_str(&json).unwrap();
576
577 assert_eq!(deserialized.tag, "data");
578 assert_eq!(deserialized.host_path, PathBuf::from("/mnt/data"));
579 assert!(deserialized.read_only);
580 }
581
582 #[test]
583 fn test_entrypoint_serde() {
584 let ep = Entrypoint {
585 executable: "/bin/sh".to_string(),
586 args: vec!["-c".to_string(), "echo hello".to_string()],
587 env: vec![
588 ("PATH".to_string(), "/usr/bin".to_string()),
589 ("HOME".to_string(), "/root".to_string()),
590 ],
591 };
592
593 let json = serde_json::to_string(&ep).unwrap();
594 let deserialized: Entrypoint = serde_json::from_str(&json).unwrap();
595
596 assert_eq!(deserialized.executable, "/bin/sh");
597 assert_eq!(deserialized.args, vec!["-c", "echo hello"]);
598 assert_eq!(deserialized.env.len(), 2);
599 }
600
601 #[test]
602 fn test_instance_spec_deserialize_missing_optional_fields() {
603 let json = r#"{
604 "box_id": "min",
605 "vcpus": 1,
606 "memory_mib": 256,
607 "rootfs_path": "/rootfs",
608 "exec_socket_path": "/exec.sock",
609 "fs_mounts": [],
610 "entrypoint": {"executable": "/bin/sh", "args": [], "env": []},
611 "console_output": null,
612 "workdir": "/"
613 }"#;
614
615 let spec: InstanceSpec = serde_json::from_str(json).unwrap();
616 assert_eq!(spec.box_id, "min");
617 assert!(spec.port_map.is_empty());
618 assert!(spec.user.is_none());
619 assert!(spec.network.is_none());
620 assert!(spec.tee_config.is_none());
621 }
622
623 #[test]
624 fn test_resource_limits_in_spec() {
625 let spec = InstanceSpec {
626 resource_limits: ResourceLimits {
627 pids_limit: Some(100),
628 cpuset_cpus: Some("0-3".to_string()),
629 ..Default::default()
630 },
631 ..Default::default()
632 };
633
634 let json = serde_json::to_string(&spec).unwrap();
635 let deserialized: InstanceSpec = serde_json::from_str(&json).unwrap();
636
637 assert_eq!(deserialized.resource_limits.pids_limit, Some(100));
638 assert_eq!(
639 deserialized.resource_limits.cpuset_cpus,
640 Some("0-3".to_string())
641 );
642 }
643
644 #[test]
645 fn test_vm_metrics_default() {
646 let m = VmMetrics::default();
647 assert!(m.cpu_percent.is_none());
648 assert!(m.memory_bytes.is_none());
649 }
650
651 #[test]
652 fn test_vm_metrics_clone() {
653 let m = VmMetrics {
654 cpu_percent: Some(50.0),
655 memory_bytes: Some(1024 * 1024),
656 };
657 let cloned = m.clone();
658 assert_eq!(cloned.cpu_percent, Some(50.0));
659 assert_eq!(cloned.memory_bytes, Some(1024 * 1024));
660 }
661}