1use std::fmt;
8
9pub const DEFAULT_CMDLINE: &str = "console=ttyAMA0 reboot=t panic=-1";
10pub const DEFAULT_MEMORY_MIB: usize = 256;
11pub const DEFAULT_VCPUS: u32 = 1;
12pub const THROUGHPUT_PROFILE_VCPUS: u32 = 4;
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum VmProfile {
16 Latency,
17 Throughput,
18}
19
20impl VmProfile {
21 pub fn parse(s: &str) -> Option<Self> {
22 match s {
23 "latency" | "low-latency" => Some(Self::Latency),
24 "throughput" => Some(Self::Throughput),
25 _ => None,
26 }
27 }
28
29 pub fn default_vcpus(self) -> u32 {
30 match self {
31 Self::Latency => DEFAULT_VCPUS,
32 Self::Throughput => THROUGHPUT_PROFILE_VCPUS,
33 }
34 }
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct VmResources {
39 pub kernel_path: Option<String>,
40 pub initrd_path: Option<String>,
41 pub cmdline: String,
42 pub memory_mib: usize,
43 pub block_devices: Vec<String>,
46 pub volumes: Vec<VolumeSpec>,
50 pub mounts: Vec<MountSpec>,
54 pub vcpus: u32,
55 pub restore_from: Option<String>,
56 pub cow_restore: bool,
57 pub snapshot: SnapshotResources,
58 pub endpoints: EndpointResources,
59 pub balloon_target_pages: Option<u32>,
67 pub tsi_token: Option<[u8; 32]>,
77}
78
79#[derive(Clone, Debug, PartialEq, Eq)]
93pub struct VolumeSpec {
94 pub host_path: String,
97 pub guest_path: String,
99 pub size_bytes: u64,
102}
103
104impl VolumeSpec {
105 pub const DEFAULT_SIZE_BYTES: u64 = 1024 * 1024 * 1024;
106
107 pub fn new(host_path: impl Into<String>, guest_path: impl Into<String>) -> Self {
108 Self {
109 host_path: host_path.into(),
110 guest_path: guest_path.into(),
111 size_bytes: Self::DEFAULT_SIZE_BYTES,
112 }
113 }
114
115 pub fn with_size_bytes(mut self, size_bytes: u64) -> Self {
116 self.size_bytes = size_bytes;
117 self
118 }
119}
120
121#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
129pub enum SymlinkPolicy {
130 Deny,
136 #[default]
144 Opaque,
145 Follow,
152}
153
154#[derive(Clone, Debug, PartialEq, Eq)]
165pub struct MountSpec {
166 pub host_path: String,
168 pub guest_tag: String,
171 pub guest_path: String,
175 pub read_only: bool,
180 pub symlinks: SymlinkPolicy,
186}
187
188impl MountSpec {
189 pub fn new(
190 host_path: impl Into<String>,
191 guest_tag: impl Into<String>,
192 guest_path: impl Into<String>,
193 ) -> Self {
194 Self {
195 host_path: host_path.into(),
196 guest_tag: guest_tag.into(),
197 guest_path: guest_path.into(),
198 read_only: false,
199 symlinks: SymlinkPolicy::default(),
200 }
201 }
202
203 pub fn read_only(mut self) -> Self {
204 self.read_only = true;
205 self
206 }
207
208 pub fn with_symlinks(mut self, p: SymlinkPolicy) -> Self {
210 self.symlinks = p;
211 self
212 }
213}
214
215#[derive(Clone, Debug, Default, PartialEq, Eq)]
216pub struct SnapshotResources {
217 pub after_ms: Option<u64>,
218 pub at_heartbeat: Option<u64>,
219 pub on_listener: bool,
220 pub on_pre_exec: bool,
230 pub quiesce_ms: u64,
231 pub out_path: Option<String>,
232}
233
234#[derive(Clone, Debug, Default, PartialEq, Eq)]
235pub struct EndpointResources {
236 pub vsock_mux: Option<String>,
237 pub http_port: Option<String>,
238 pub vsock_mux_handoff: Option<String>,
243 pub vsock_exec: Option<String>,
249 pub vsock_exec_guest_port: Option<u32>,
252}
253
254pub const DEFAULT_EXEC_GUEST_PORT: u32 = 1028;
259
260#[derive(Clone, Debug, PartialEq, Eq)]
261pub enum ResourceError {
262 MissingKernel,
263 ZeroMemory,
264 ZeroVcpus,
265 SnapshotTriggerWithoutOutput,
266}
267
268impl VmResources {
269 pub fn new() -> Self {
270 Self::default()
271 }
272
273 pub fn for_kernel(kernel_path: impl Into<String>, initrd_path: impl Into<String>) -> Self {
274 Self::new()
275 .with_kernel_path(kernel_path)
276 .with_initramfs(initrd_path)
277 }
278
279 pub fn from_snapshot(path: impl Into<String>) -> Self {
280 Self::new().with_restore(path)
281 }
282
283 pub fn with_kernel_path(mut self, path: impl Into<String>) -> Self {
284 self.kernel_path = Some(path.into());
285 self
286 }
287
288 pub fn with_initramfs(mut self, path: impl Into<String>) -> Self {
289 self.initrd_path = Some(path.into());
290 self
291 }
292
293 pub fn with_cmdline(mut self, cmdline: impl Into<String>) -> Self {
294 self.cmdline = cmdline.into();
295 self
296 }
297
298 pub fn with_memory_mib(mut self, memory_mib: usize) -> Self {
299 self.memory_mib = memory_mib;
300 self
301 }
302
303 pub fn with_profile(mut self, profile: VmProfile) -> Self {
304 self.apply_profile_defaults(profile);
305 self
306 }
307
308 pub fn with_vcpus(mut self, vcpus: u32) -> Self {
309 self.vcpus = vcpus;
310 self
311 }
312
313 pub fn with_block_device(mut self, path: impl Into<String>) -> Self {
314 self.block_devices.push(path.into());
315 self
316 }
317
318 pub fn with_volume(mut self, volume: VolumeSpec) -> Self {
320 self.volumes.push(volume);
321 self
322 }
323
324 pub fn with_mount(mut self, mount: MountSpec) -> Self {
326 self.mounts.push(mount);
327 self
328 }
329
330 pub fn with_restore(mut self, path: impl Into<String>) -> Self {
331 self.restore_from = Some(path.into());
332 self
333 }
334
335 pub fn with_cow_restore(mut self, enabled: bool) -> Self {
336 self.cow_restore = enabled;
337 self
338 }
339
340 pub fn with_snapshot_after_ms(mut self, after_ms: u64, out_path: impl Into<String>) -> Self {
341 self.snapshot.after_ms = Some(after_ms);
342 self.snapshot.out_path = Some(out_path.into());
343 self
344 }
345
346 pub fn with_snapshot_at_heartbeat(
347 mut self,
348 at_heartbeat: u64,
349 out_path: impl Into<String>,
350 ) -> Self {
351 self.snapshot.at_heartbeat = Some(at_heartbeat);
352 self.snapshot.out_path = Some(out_path.into());
353 self
354 }
355
356 pub fn with_snapshot_on_listener(mut self, out_path: impl Into<String>) -> Self {
357 self.snapshot.on_listener = true;
358 self.snapshot.out_path = Some(out_path.into());
359 self
360 }
361
362 pub fn with_snapshot_on_pre_exec(mut self, out_path: impl Into<String>) -> Self {
367 self.snapshot.on_pre_exec = true;
368 self.snapshot.out_path = Some(out_path.into());
369 self
370 }
371
372 pub fn with_quiesce_ms(mut self, quiesce_ms: u64) -> Self {
373 self.snapshot.quiesce_ms = quiesce_ms;
374 self
375 }
376
377 pub fn with_vsock_mux(mut self, path: impl Into<String>) -> Self {
378 self.endpoints.vsock_mux = Some(path.into());
379 self
380 }
381
382 pub fn with_http_port(mut self, port: impl Into<String>) -> Self {
383 self.endpoints.http_port = Some(port.into());
384 self
385 }
386
387 pub fn with_vsock_mux_handoff(mut self, path: impl Into<String>) -> Self {
388 self.endpoints.vsock_mux_handoff = Some(path.into());
389 self
390 }
391
392 pub fn with_vsock_exec(mut self, path: impl Into<String>) -> Self {
395 self.endpoints.vsock_exec = Some(path.into());
396 self
397 }
398
399 pub fn with_vsock_exec_guest_port(mut self, port: u32) -> Self {
403 self.endpoints.vsock_exec_guest_port = Some(port);
404 self
405 }
406
407 pub fn with_tsi_token(mut self, token: Option<[u8; 32]>) -> Self {
412 self.tsi_token = token;
413 self
414 }
415
416 pub fn memory_bytes(&self) -> usize {
417 self.memory_mib * 1024 * 1024
418 }
419
420 pub fn is_restore(&self) -> bool {
421 self.restore_from.is_some()
422 }
423
424 pub fn apply_profile_defaults(&mut self, profile: VmProfile) {
425 self.vcpus = profile.default_vcpus();
426 }
427
428 pub fn validate_for_run(&self) -> Result<(), ResourceError> {
429 if self.memory_mib == 0 {
430 return Err(ResourceError::ZeroMemory);
431 }
432 if self.vcpus == 0 {
433 return Err(ResourceError::ZeroVcpus);
434 }
435 if self.kernel_path.is_none() && self.restore_from.is_none() {
436 return Err(ResourceError::MissingKernel);
437 }
438 let wants_snapshot = self.snapshot.after_ms.is_some()
439 || self.snapshot.at_heartbeat.is_some()
440 || self.snapshot.on_listener
441 || self.snapshot.on_pre_exec;
442 if wants_snapshot && self.snapshot.out_path.is_none() {
443 return Err(ResourceError::SnapshotTriggerWithoutOutput);
444 }
445 Ok(())
446 }
447}
448
449impl Default for VmResources {
450 fn default() -> Self {
451 Self {
452 kernel_path: None,
453 initrd_path: None,
454 cmdline: DEFAULT_CMDLINE.to_string(),
455 memory_mib: DEFAULT_MEMORY_MIB,
456 block_devices: Vec::new(),
457 volumes: Vec::new(),
458 mounts: Vec::new(),
459 vcpus: DEFAULT_VCPUS,
460 restore_from: None,
461 cow_restore: false,
462 snapshot: SnapshotResources::default(),
463 endpoints: EndpointResources::default(),
464 balloon_target_pages: None,
465 tsi_token: None,
466 }
467 }
468}
469
470impl fmt::Display for ResourceError {
471 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472 match self {
473 ResourceError::MissingKernel => {
474 write!(f, "kernel path or restore snapshot is required")
475 }
476 ResourceError::ZeroMemory => write!(f, "memory must be greater than zero"),
477 ResourceError::ZeroVcpus => write!(f, "vCPU count must be greater than zero"),
478 ResourceError::SnapshotTriggerWithoutOutput => {
479 write!(f, "snapshot trigger requires snapshot output path")
480 }
481 }
482 }
483}
484
485impl std::error::Error for ResourceError {}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490
491 #[test]
492 fn defaults_match_supermachine_cli() {
493 let resources = VmResources::default();
494 assert_eq!(resources.cmdline, DEFAULT_CMDLINE);
495 assert_eq!(resources.memory_mib, 256);
496 assert_eq!(resources.vcpus, 1);
497 assert_eq!(resources.memory_bytes(), 256 * 1024 * 1024);
498 }
499
500 #[test]
501 fn profile_defaults_are_stable() {
502 assert_eq!(VmProfile::parse("latency"), Some(VmProfile::Latency));
503 assert_eq!(VmProfile::parse("low-latency"), Some(VmProfile::Latency));
504 assert_eq!(VmProfile::parse("throughput"), Some(VmProfile::Throughput));
505 assert_eq!(VmProfile::parse("unknown"), None);
506 assert_eq!(VmProfile::Latency.default_vcpus(), 1);
507 assert_eq!(VmProfile::Throughput.default_vcpus(), 4);
508 }
509
510 #[test]
511 fn applies_profile_defaults_to_resources() {
512 let mut resources = VmResources::default();
513 resources.apply_profile_defaults(VmProfile::Throughput);
514 assert_eq!(resources.vcpus, 4);
515 resources.apply_profile_defaults(VmProfile::Latency);
516 assert_eq!(resources.vcpus, 1);
517 }
518
519 #[test]
520 fn convenience_constructors_cover_kernel_and_restore() {
521 let kernel = VmResources::for_kernel("kernel", "initrd");
522 assert_eq!(kernel.kernel_path.as_deref(), Some("kernel"));
523 assert_eq!(kernel.initrd_path.as_deref(), Some("initrd"));
524 assert!(!kernel.is_restore());
525
526 let restore = VmResources::from_snapshot("snap.sm");
527 assert_eq!(restore.restore_from.as_deref(), Some("snap.sm"));
528 assert!(restore.is_restore());
529 }
530
531 #[test]
532 fn builder_style_methods_cover_common_library_config() {
533 let resources = VmResources::new()
534 .with_kernel_path("kernel")
535 .with_initramfs("initrd")
536 .with_cmdline("console=ttyS0")
537 .with_memory_mib(512)
538 .with_profile(VmProfile::Throughput)
539 .with_vcpus(2)
540 .with_block_device("rootfs.squashfs")
541 .with_snapshot_at_heartbeat(1, "snap.sm")
542 .with_quiesce_ms(7)
543 .with_vsock_mux("/tmp/vsock.sock")
544 .with_http_port("8080");
545
546 assert_eq!(resources.kernel_path.as_deref(), Some("kernel"));
547 assert_eq!(resources.initrd_path.as_deref(), Some("initrd"));
548 assert_eq!(resources.cmdline, "console=ttyS0");
549 assert_eq!(resources.memory_mib, 512);
550 assert_eq!(resources.vcpus, 2);
551 assert_eq!(resources.block_devices, vec!["rootfs.squashfs"]);
552 assert_eq!(resources.snapshot.at_heartbeat, Some(1));
553 assert_eq!(resources.snapshot.quiesce_ms, 7);
554 assert_eq!(resources.snapshot.out_path.as_deref(), Some("snap.sm"));
555 assert_eq!(
556 resources.endpoints.vsock_mux.as_deref(),
557 Some("/tmp/vsock.sock")
558 );
559 assert_eq!(resources.endpoints.http_port.as_deref(), Some("8080"));
560 }
561
562 #[test]
563 fn builder_style_restore_config_is_valid_without_kernel() {
564 let resources = VmResources::new()
565 .with_restore("snap.sm")
566 .with_cow_restore(true);
567
568 assert!(resources.is_restore());
569 assert!(resources.cow_restore);
570 assert_eq!(resources.validate_for_run(), Ok(()));
571 }
572
573 #[test]
574 fn validates_kernel_or_restore() {
575 let mut resources = VmResources::default();
576 assert_eq!(
577 resources.validate_for_run(),
578 Err(ResourceError::MissingKernel)
579 );
580
581 resources.kernel_path = Some("vmlinux".to_string());
582 assert_eq!(resources.validate_for_run(), Ok(()));
583
584 resources.kernel_path = None;
585 resources.restore_from = Some("snap.sm".to_string());
586 assert_eq!(resources.validate_for_run(), Ok(()));
587 }
588
589 #[test]
592 fn mount_spec_constructs_with_required_guest_path() {
593 let m = MountSpec::new("/host/x", "tag", "/workspace");
594 assert_eq!(m.host_path, "/host/x");
595 assert_eq!(m.guest_tag, "tag");
596 assert_eq!(m.guest_path, "/workspace");
597 assert_eq!(m.symlinks, SymlinkPolicy::Opaque);
598 assert!(!m.read_only);
599 }
600
601 #[test]
602 fn mount_spec_symlinks_composes_with_construction() {
603 let m = MountSpec::new("/h", "t", "/w").with_symlinks(SymlinkPolicy::Deny);
604 assert_eq!(m.guest_path, "/w");
605 assert_eq!(m.symlinks, SymlinkPolicy::Deny);
606 }
607
608 #[test]
609 fn mount_spec_read_only_composes_with_construction() {
610 let m = MountSpec::new("/h", "t", "/w").read_only();
611 assert_eq!(m.guest_path, "/w");
612 assert!(m.read_only);
613 }
614
615 #[test]
616 fn mount_spec_constructor_accepts_string_types() {
617 let m_str = MountSpec::new("/h", "t", "/abs");
620 let m_string = MountSpec::new(String::from("/h"), String::from("t"), String::from("/abs"));
621 assert_eq!(m_str.guest_path, m_string.guest_path);
622 assert_eq!(m_str.host_path, m_string.host_path);
623 assert_eq!(m_str.guest_tag, m_string.guest_tag);
624 }
625
626 #[test]
627 fn volume_spec_basic_construction() {
628 let v = VolumeSpec::new("/host/nm.img", "/workspace/node_modules");
629 assert_eq!(v.host_path, "/host/nm.img");
630 assert_eq!(v.guest_path, "/workspace/node_modules");
631 assert_eq!(v.size_bytes, VolumeSpec::DEFAULT_SIZE_BYTES);
632 }
633
634 #[test]
635 fn volume_spec_with_size_bytes() {
636 let v = VolumeSpec::new("/h", "/g").with_size_bytes(4 * 1024 * 1024 * 1024);
637 assert_eq!(v.size_bytes, 4 * 1024 * 1024 * 1024);
638 }
639}