1#[cfg(windows)]
6pub mod hyperv;
7pub mod openvmm;
9pub mod vtl2_settings;
10
11use crate::PetriLogSource;
12use crate::PetriTestParams;
13use crate::ShutdownKind;
14use crate::disk_image::AgentImage;
15use crate::disk_image::SECTOR_SIZE;
16use crate::openhcl_diag::OpenHclDiagHandler;
17use crate::test::PetriPostTestHook;
18use crate::vtl2_settings::ControllerType;
19use crate::vtl2_settings::Vtl2LunBuilder;
20use crate::vtl2_settings::Vtl2StorageBackingDeviceBuilder;
21use crate::vtl2_settings::Vtl2StorageControllerBuilder;
22use async_trait::async_trait;
23use get_resources::ged::FirmwareEvent;
24use guid::Guid;
25use memory_range::MemoryRange;
26use mesh::CancelContext;
27use openvmm_defs::config::Vtl2BaseAddressType;
28use pal_async::DefaultDriver;
29use pal_async::task::Spawn;
30use pal_async::task::Task;
31use pal_async::timer::PolledTimer;
32use petri_artifacts_common::tags::GuestQuirks;
33use petri_artifacts_common::tags::GuestQuirksInner;
34use petri_artifacts_common::tags::InitialRebootCondition;
35use petri_artifacts_common::tags::IsOpenhclIgvm;
36use petri_artifacts_common::tags::IsTestVmgs;
37use petri_artifacts_common::tags::MachineArch;
38use petri_artifacts_common::tags::OsFlavor;
39use petri_artifacts_core::ArtifactResolver;
40use petri_artifacts_core::ArtifactSource;
41use petri_artifacts_core::ResolvedArtifact;
42use petri_artifacts_core::ResolvedArtifactSource;
43use petri_artifacts_core::ResolvedOptionalArtifact;
44use pipette_client::PipetteClient;
45use std::collections::BTreeMap;
46use std::collections::HashMap;
47use std::collections::hash_map::DefaultHasher;
48use std::fmt::Debug;
49use std::hash::Hash;
50use std::hash::Hasher;
51use std::path::Path;
52use std::path::PathBuf;
53use std::sync::Arc;
54use std::time::Duration;
55use tempfile::TempPath;
56use vmgs_resources::GuestStateEncryptionPolicy;
57use vtl2_settings_proto::StorageController;
58use vtl2_settings_proto::Vtl2Settings;
59
60pub struct PetriVmArtifacts<T: PetriVmmBackend> {
63 pub backend: T,
65 pub firmware: Firmware,
67 pub arch: MachineArch,
69 pub agent_image: Option<AgentImage>,
71 pub openhcl_agent_image: Option<AgentImage>,
73 pub pipette_binary: Option<ResolvedArtifact>,
75}
76
77impl<T: PetriVmmBackend> PetriVmArtifacts<T> {
78 pub fn new(
82 resolver: &ArtifactResolver<'_>,
83 firmware: Firmware,
84 arch: MachineArch,
85 with_vtl0_pipette: bool,
86 ) -> Option<Self> {
87 if !T::check_compat(&firmware, arch) {
88 return None;
89 }
90
91 let pipette_binary = if with_vtl0_pipette {
92 Some(Self::resolve_pipette_binary(
93 resolver,
94 firmware.os_flavor(),
95 arch,
96 ))
97 } else {
98 None
99 };
100
101 Some(Self {
102 backend: T::new(resolver),
103 arch,
104 agent_image: Some(if with_vtl0_pipette {
105 AgentImage::new(firmware.os_flavor()).with_pipette(resolver, arch)
106 } else {
107 AgentImage::new(firmware.os_flavor())
108 }),
109 openhcl_agent_image: if firmware.is_openhcl() {
110 Some(AgentImage::new(OsFlavor::Linux).with_pipette(resolver, arch))
111 } else {
112 None
113 },
114 pipette_binary,
115 firmware,
116 })
117 }
118
119 fn resolve_pipette_binary(
120 resolver: &ArtifactResolver<'_>,
121 os_flavor: OsFlavor,
122 arch: MachineArch,
123 ) -> ResolvedArtifact {
124 use petri_artifacts_common::artifacts as common_artifacts;
125 match (os_flavor, arch) {
126 (OsFlavor::Linux, MachineArch::X86_64) => resolver
127 .require(common_artifacts::PIPETTE_LINUX_X64)
128 .erase(),
129 (OsFlavor::Linux, MachineArch::Aarch64) => resolver
130 .require(common_artifacts::PIPETTE_LINUX_AARCH64)
131 .erase(),
132 (OsFlavor::Windows, MachineArch::X86_64) => resolver
133 .require(common_artifacts::PIPETTE_WINDOWS_X64)
134 .erase(),
135 (OsFlavor::Windows, MachineArch::Aarch64) => resolver
136 .require(common_artifacts::PIPETTE_WINDOWS_AARCH64)
137 .erase(),
138 (OsFlavor::FreeBsd | OsFlavor::Uefi, _) => {
139 panic!("No pipette binary for this OS flavor")
140 }
141 }
142 }
143}
144
145pub struct PetriVmBuilder<T: PetriVmmBackend> {
147 backend: T,
149 config: PetriVmConfig,
151 modify_vmm_config: Option<ModifyFn<T::VmmConfig>>,
153 resources: PetriVmResources,
155
156 guest_quirks: GuestQuirksInner,
158 vmm_quirks: VmmQuirks,
159
160 expected_boot_event: Option<FirmwareEvent>,
163 override_expect_reset: bool,
164
165 agent_image: Option<AgentImage>,
169 openhcl_agent_image: Option<AgentImage>,
171 boot_device_type: BootDeviceType,
173
174 minimal_mode: bool,
176 pipette_binary: Option<ResolvedArtifact>,
178 enable_serial: bool,
180 enable_screenshots: bool,
182 prebuilt_initrd: Option<PathBuf>,
184}
185
186impl<T: PetriVmmBackend> Debug for PetriVmBuilder<T> {
187 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188 f.debug_struct("PetriVmBuilder")
189 .field("backend", &self.backend)
190 .field("config", &self.config)
191 .field("modify_vmm_config", &self.modify_vmm_config.is_some())
192 .field("resources", &self.resources)
193 .field("guest_quirks", &self.guest_quirks)
194 .field("vmm_quirks", &self.vmm_quirks)
195 .field("expected_boot_event", &self.expected_boot_event)
196 .field("override_expect_reset", &self.override_expect_reset)
197 .field("agent_image", &self.agent_image)
198 .field("openhcl_agent_image", &self.openhcl_agent_image)
199 .field("boot_device_type", &self.boot_device_type)
200 .field("minimal_mode", &self.minimal_mode)
201 .field("enable_serial", &self.enable_serial)
202 .field("enable_screenshots", &self.enable_screenshots)
203 .field("prebuilt_initrd", &self.prebuilt_initrd)
204 .finish()
205 }
206}
207
208#[derive(Debug)]
210pub struct PetriVmConfig {
211 pub name: String,
213 pub arch: MachineArch,
215 pub host_log_levels: Option<OpenvmmLogConfig>,
217 pub firmware: Firmware,
219 pub memory: MemoryConfig,
221 pub proc_topology: ProcessorTopology,
223 pub vmgs: PetriVmgsResource,
225 pub tpm: Option<TpmConfig>,
227 pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
229 pub pcie_nvme_drives: Vec<PcieNvmeDrive>,
231}
232
233#[derive(Debug)]
235pub struct PcieNvmeDrive {
236 pub port_name: String,
238 pub nsid: u32,
240 pub drive: Drive,
242}
243
244pub struct PetriVmProperties {
247 pub is_openhcl: bool,
249 pub is_isolated: bool,
251 pub is_pcat: bool,
253 pub is_linux_direct: bool,
255 pub using_vtl0_pipette: bool,
257 pub using_vpci: bool,
259 pub os_flavor: OsFlavor,
261 pub minimal_mode: bool,
263 pub uses_pipette_as_init: bool,
265 pub enable_serial: bool,
267 pub prebuilt_initrd: Option<PathBuf>,
269 pub has_agent_disk: bool,
271}
272
273pub struct PetriVmRuntimeConfig {
275 pub vtl2_settings: Option<Vtl2Settings>,
277 pub ide_controllers: Option<[[Option<Drive>; 2]; 2]>,
279 pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
281}
282
283#[derive(Debug)]
285pub struct PetriVmResources {
286 driver: DefaultDriver,
287 log_source: PetriLogSource,
288}
289
290#[async_trait]
292pub trait PetriVmmBackend: Debug {
293 type VmmConfig;
295
296 type VmRuntime: PetriVmRuntime;
298
299 fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool;
302
303 fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks);
305
306 fn default_servicing_flags() -> OpenHclServicingFlags;
308
309 fn create_guest_dump_disk() -> anyhow::Result<
312 Option<(
313 Arc<TempPath>,
314 Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
315 )>,
316 >;
317
318 fn new(resolver: &ArtifactResolver<'_>) -> Self;
320
321 async fn run(
323 self,
324 config: PetriVmConfig,
325 modify_vmm_config: Option<ModifyFn<Self::VmmConfig>>,
326 resources: &PetriVmResources,
327 properties: PetriVmProperties,
328 ) -> anyhow::Result<(Self::VmRuntime, PetriVmRuntimeConfig)>;
329}
330
331pub(crate) const PETRI_IDE_BOOT_CONTROLLER_NUMBER: u32 = 0;
333pub(crate) const PETRI_IDE_BOOT_LUN: u8 = 0;
334pub(crate) const PETRI_IDE_BOOT_CONTROLLER: Guid =
335 guid::guid!("ca56751f-e643-4bef-bf54-f73678e8b7b5");
336
337pub(crate) const PETRI_SCSI_BOOT_LUN: u32 = 0;
339pub(crate) const PETRI_SCSI_PIPETTE_LUN: u32 = 1;
340pub(crate) const PETRI_SCSI_CRASH_LUN: u32 = 2;
341pub(crate) const PETRI_SCSI_VTL0_CONTROLLER: Guid =
343 guid::guid!("27b553e8-8b39-411b-a55f-839971a7884f");
344pub(crate) const PETRI_SCSI_VTL2_CONTROLLER: Guid =
346 guid::guid!("766e96f8-2ceb-437e-afe3-a93169e48a7c");
347pub(crate) const PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER: Guid =
349 guid::guid!("6c474f47-ed39-49e6-bbb9-142177a1da6e");
350
351pub(crate) const PETRI_NVME_BOOT_NSID: u32 = 37;
353pub(crate) const PETRI_NVME_BOOT_VTL0_CONTROLLER: Guid =
355 guid::guid!("e23a04e2-90f5-4852-bc9d-e7ac691b756c");
356pub(crate) const PETRI_NVME_BOOT_VTL2_CONTROLLER: Guid =
358 guid::guid!("92bc8346-718b-449a-8751-edbf3dcd27e4");
359
360pub struct PetriVm<T: PetriVmmBackend> {
362 resources: PetriVmResources,
363 runtime: T::VmRuntime,
364 watchdog_tasks: Vec<Task<()>>,
365 openhcl_diag_handler: Option<OpenHclDiagHandler>,
366
367 arch: MachineArch,
368 guest_quirks: GuestQuirksInner,
369 vmm_quirks: VmmQuirks,
370 expected_boot_event: Option<FirmwareEvent>,
371 uses_pipette_as_init: bool,
372
373 config: PetriVmRuntimeConfig,
374}
375
376impl<T: PetriVmmBackend> PetriVmBuilder<T> {
377 pub fn new(
379 params: PetriTestParams<'_>,
380 artifacts: PetriVmArtifacts<T>,
381 driver: &DefaultDriver,
382 ) -> anyhow::Result<Self> {
383 let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
384 let expected_boot_event = artifacts.firmware.expected_boot_event();
385 let boot_device_type = match artifacts.firmware {
386 Firmware::LinuxDirect { .. } => BootDeviceType::None,
387 Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
388 Firmware::Pcat { .. } => BootDeviceType::Ide,
389 Firmware::OpenhclPcat { .. } => BootDeviceType::IdeViaScsi,
390 Firmware::Uefi {
391 guest: UefiGuest::None,
392 ..
393 }
394 | Firmware::OpenhclUefi {
395 guest: UefiGuest::None,
396 ..
397 } => BootDeviceType::None,
398 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
399 };
400
401 Ok(Self {
402 backend: artifacts.backend,
403 config: PetriVmConfig {
404 name: make_vm_safe_name(params.test_name),
405 arch: artifacts.arch,
406 host_log_levels: None,
407 firmware: artifacts.firmware,
408 memory: Default::default(),
409 proc_topology: Default::default(),
410
411 vmgs: PetriVmgsResource::Ephemeral,
412 tpm: None,
413 vmbus_storage_controllers: HashMap::new(),
414 pcie_nvme_drives: Vec::new(),
415 },
416 modify_vmm_config: None,
417 resources: PetriVmResources {
418 driver: driver.clone(),
419 log_source: params.logger.clone(),
420 },
421
422 guest_quirks,
423 vmm_quirks,
424 expected_boot_event,
425 override_expect_reset: false,
426
427 agent_image: artifacts.agent_image,
428 openhcl_agent_image: artifacts.openhcl_agent_image,
429 boot_device_type,
430
431 minimal_mode: false,
432 pipette_binary: artifacts.pipette_binary,
433 enable_serial: true,
434 enable_screenshots: true,
435 prebuilt_initrd: None,
436 }
437 .add_petri_scsi_controllers()
438 .add_guest_crash_disk(params.post_test_hooks))
439 }
440
441 pub fn minimal(
452 params: PetriTestParams<'_>,
453 artifacts: PetriVmArtifacts<T>,
454 driver: &DefaultDriver,
455 ) -> anyhow::Result<Self> {
456 let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
457 let expected_boot_event = artifacts.firmware.expected_boot_event();
458 let boot_device_type = match artifacts.firmware {
459 Firmware::LinuxDirect { .. } => BootDeviceType::None,
460 Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
461 Firmware::Pcat { .. } => BootDeviceType::Ide,
462 Firmware::OpenhclPcat { .. } => BootDeviceType::IdeViaScsi,
463 Firmware::Uefi {
464 guest: UefiGuest::None,
465 ..
466 }
467 | Firmware::OpenhclUefi {
468 guest: UefiGuest::None,
469 ..
470 } => BootDeviceType::None,
471 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
472 };
473
474 Ok(Self {
475 backend: artifacts.backend,
476 config: PetriVmConfig {
477 name: make_vm_safe_name(params.test_name),
478 arch: artifacts.arch,
479 host_log_levels: None,
480 firmware: artifacts.firmware,
481 memory: Default::default(),
482 proc_topology: Default::default(),
483
484 vmgs: PetriVmgsResource::Ephemeral,
485 tpm: None,
486 vmbus_storage_controllers: HashMap::new(),
487 pcie_nvme_drives: Vec::new(),
488 },
489 modify_vmm_config: None,
490 resources: PetriVmResources {
491 driver: driver.clone(),
492 log_source: params.logger.clone(),
493 },
494
495 guest_quirks,
496 vmm_quirks,
497 expected_boot_event,
498 override_expect_reset: false,
499
500 agent_image: artifacts.agent_image,
501 openhcl_agent_image: artifacts.openhcl_agent_image,
502 boot_device_type,
503
504 minimal_mode: true,
505 pipette_binary: artifacts.pipette_binary,
506 enable_serial: false,
507 enable_screenshots: true,
508 prebuilt_initrd: None,
509 })
510 }
511
512 pub fn is_minimal(&self) -> bool {
514 self.minimal_mode
515 }
516
517 pub fn with_prebuilt_initrd(mut self, path: PathBuf) -> Self {
524 self.prebuilt_initrd = Some(path);
525 self
526 }
527
528 pub fn prepare_initrd(&self) -> anyhow::Result<TempPath> {
539 use anyhow::Context;
540 use std::io::Write;
541
542 let initrd_path = self
543 .config
544 .firmware
545 .linux_direct_initrd()
546 .context("prepare_initrd requires Linux direct boot with initrd")?;
547 let pipette_path = self
548 .pipette_binary
549 .as_ref()
550 .context("prepare_initrd requires a pipette binary")?;
551
552 let initrd_gz = std::fs::read(initrd_path)
553 .with_context(|| format!("failed to read initrd at {}", initrd_path.display()))?;
554 let pipette_data = std::fs::read(pipette_path.get()).with_context(|| {
555 format!(
556 "failed to read pipette binary at {}",
557 pipette_path.get().display()
558 )
559 })?;
560
561 let merged_gz =
562 crate::cpio::inject_into_initrd(&initrd_gz, "pipette", &pipette_data, 0o100755)
563 .context("failed to inject pipette into initrd")?;
564
565 let mut tmp = tempfile::NamedTempFile::new()
566 .context("failed to create temp file for pre-built initrd")?;
567 tmp.write_all(&merged_gz)
568 .context("failed to write pre-built initrd")?;
569
570 Ok(tmp.into_temp_path())
571 }
572
573 pub fn with_serial_output(mut self) -> Self {
582 self.enable_serial = true;
583 self
584 }
585
586 pub fn without_serial_output(mut self) -> Self {
591 self.enable_serial = false;
592 self
593 }
594
595 pub fn without_screenshots(mut self) -> Self {
600 self.enable_screenshots = false;
601 self
602 }
603
604 fn add_petri_scsi_controllers(self) -> Self {
605 let builder = self.add_vmbus_storage_controller(
606 &PETRI_SCSI_VTL0_CONTROLLER,
607 Vtl::Vtl0,
608 VmbusStorageType::Scsi,
609 );
610
611 if builder.is_openhcl() {
612 builder.add_vmbus_storage_controller(
613 &PETRI_SCSI_VTL2_CONTROLLER,
614 Vtl::Vtl2,
615 VmbusStorageType::Scsi,
616 )
617 } else {
618 builder
619 }
620 }
621
622 fn add_guest_crash_disk(self, post_test_hooks: &mut Vec<PetriPostTestHook>) -> Self {
623 let logger = self.resources.log_source.clone();
624 let (disk, disk_hook) = matches!(
625 self.config.firmware.os_flavor(),
626 OsFlavor::Windows | OsFlavor::Linux
627 )
628 .then(|| T::create_guest_dump_disk().expect("failed to create guest dump disk"))
629 .flatten()
630 .unzip();
631
632 if let Some(disk_hook) = disk_hook {
633 post_test_hooks.push(PetriPostTestHook::new(
634 "extract guest crash dumps".into(),
635 move |test_passed| {
636 if test_passed {
637 return Ok(());
638 }
639 let mut disk = disk_hook()?;
640 let gpt = gptman::GPT::read_from(&mut disk, SECTOR_SIZE)?;
641 let partition = fscommon::StreamSlice::new(
642 &mut disk,
643 gpt[1].starting_lba * SECTOR_SIZE,
644 gpt[1].ending_lba * SECTOR_SIZE,
645 )?;
646 let fs = fatfs::FileSystem::new(partition, fatfs::FsOptions::new())?;
647 for entry in fs.root_dir().iter() {
648 let Ok(entry) = entry else {
649 tracing::warn!(?entry, "failed to read entry in guest crash dump disk");
650 continue;
651 };
652 if !entry.is_file() {
653 tracing::warn!(
654 ?entry,
655 "skipping non-file entry in guest crash dump disk"
656 );
657 continue;
658 }
659 logger.write_attachment(&entry.file_name(), entry.to_file())?;
660 }
661 Ok(())
662 },
663 ));
664 }
665
666 if let Some(disk) = disk {
667 self.add_vmbus_drive(
668 Drive::new(Some(Disk::Temporary(disk)), false),
669 &PETRI_SCSI_VTL0_CONTROLLER,
670 Some(PETRI_SCSI_CRASH_LUN),
671 )
672 } else {
673 self
674 }
675 }
676
677 fn add_agent_disks(self) -> Self {
678 self.add_agent_disk_inner(Vtl::Vtl0)
679 .add_agent_disk_inner(Vtl::Vtl2)
680 }
681
682 fn add_agent_disk_inner(mut self, target_vtl: Vtl) -> Self {
683 let (agent_image, controller_id) = match target_vtl {
684 Vtl::Vtl0 => (self.agent_image.as_ref(), PETRI_SCSI_VTL0_CONTROLLER),
685 Vtl::Vtl1 => panic!("no VTL1 agent disk"),
686 Vtl::Vtl2 => (
687 self.openhcl_agent_image.as_ref(),
688 PETRI_SCSI_VTL2_CONTROLLER,
689 ),
690 };
691
692 if target_vtl == Vtl::Vtl0
695 && self.uses_pipette_as_init()
696 && !agent_image.is_some_and(|i| i.has_extras())
697 {
698 return self;
699 }
700
701 let Some(agent_disk) = agent_image.and_then(|i| {
702 i.build(crate::disk_image::ImageType::Vhd)
703 .expect("failed to build agent image")
704 }) else {
705 return self;
706 };
707
708 if !self
711 .config
712 .vmbus_storage_controllers
713 .contains_key(&controller_id)
714 {
715 self = self.add_vmbus_storage_controller(
716 &controller_id,
717 target_vtl,
718 VmbusStorageType::Scsi,
719 );
720 }
721
722 self.add_vmbus_drive(
723 Drive::new(
724 Some(Disk::Temporary(Arc::new(agent_disk.into_temp_path()))),
725 false,
726 ),
727 &controller_id,
728 Some(PETRI_SCSI_PIPETTE_LUN),
729 )
730 }
731
732 fn add_boot_disk(mut self) -> Self {
733 if self.boot_device_type.requires_vtl2() && !self.is_openhcl() {
734 panic!("boot device type {:?} requires vtl2", self.boot_device_type);
735 }
736
737 if self.boot_device_type.requires_vpci_boot() {
738 self.config
739 .firmware
740 .uefi_config_mut()
741 .expect("vpci boot requires uefi")
742 .enable_vpci_boot = true;
743 }
744
745 if let Some(boot_drive) = self.config.firmware.boot_drive() {
746 match self.boot_device_type {
747 BootDeviceType::None => unreachable!(),
748 BootDeviceType::Ide => self.add_ide_drive(
749 boot_drive,
750 PETRI_IDE_BOOT_CONTROLLER_NUMBER,
751 PETRI_IDE_BOOT_LUN,
752 ),
753 BootDeviceType::IdeViaScsi => self
754 .add_vmbus_drive(
755 boot_drive,
756 &PETRI_SCSI_VTL2_CONTROLLER,
757 Some(PETRI_SCSI_BOOT_LUN),
758 )
759 .add_vtl2_storage_controller(
760 Vtl2StorageControllerBuilder::new(ControllerType::Ide)
761 .with_instance_id(PETRI_IDE_BOOT_CONTROLLER)
762 .add_lun(
763 Vtl2LunBuilder::disk()
764 .with_channel(PETRI_IDE_BOOT_CONTROLLER_NUMBER)
765 .with_location(PETRI_IDE_BOOT_LUN as u32)
766 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
767 ControllerType::Scsi,
768 PETRI_SCSI_VTL2_CONTROLLER,
769 PETRI_SCSI_BOOT_LUN,
770 )),
771 )
772 .build(),
773 ),
774 BootDeviceType::IdeViaNvme => todo!(),
775 BootDeviceType::Scsi => self.add_vmbus_drive(
776 boot_drive,
777 &PETRI_SCSI_VTL0_CONTROLLER,
778 Some(PETRI_SCSI_BOOT_LUN),
779 ),
780 BootDeviceType::ScsiViaScsi => self
781 .add_vmbus_drive(
782 boot_drive,
783 &PETRI_SCSI_VTL2_CONTROLLER,
784 Some(PETRI_SCSI_BOOT_LUN),
785 )
786 .add_vtl2_storage_controller(
787 Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
788 .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
789 .add_lun(
790 Vtl2LunBuilder::disk()
791 .with_location(PETRI_SCSI_BOOT_LUN)
792 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
793 ControllerType::Scsi,
794 PETRI_SCSI_VTL2_CONTROLLER,
795 PETRI_SCSI_BOOT_LUN,
796 )),
797 )
798 .build(),
799 ),
800 BootDeviceType::ScsiViaNvme => self
801 .add_vmbus_storage_controller(
802 &PETRI_NVME_BOOT_VTL2_CONTROLLER,
803 Vtl::Vtl2,
804 VmbusStorageType::Nvme,
805 )
806 .add_vmbus_drive(
807 boot_drive,
808 &PETRI_NVME_BOOT_VTL2_CONTROLLER,
809 Some(PETRI_NVME_BOOT_NSID),
810 )
811 .add_vtl2_storage_controller(
812 Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
813 .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
814 .add_lun(
815 Vtl2LunBuilder::disk()
816 .with_location(PETRI_SCSI_BOOT_LUN)
817 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
818 ControllerType::Nvme,
819 PETRI_NVME_BOOT_VTL2_CONTROLLER,
820 PETRI_NVME_BOOT_NSID,
821 )),
822 )
823 .build(),
824 ),
825 BootDeviceType::Nvme => self
826 .add_vmbus_storage_controller(
827 &PETRI_NVME_BOOT_VTL0_CONTROLLER,
828 Vtl::Vtl0,
829 VmbusStorageType::Nvme,
830 )
831 .add_vmbus_drive(
832 boot_drive,
833 &PETRI_NVME_BOOT_VTL0_CONTROLLER,
834 Some(PETRI_NVME_BOOT_NSID),
835 ),
836 BootDeviceType::NvmeViaScsi => todo!(),
837 BootDeviceType::NvmeViaNvme => todo!(),
838 BootDeviceType::PcieNvme => {
839 self.config.pcie_nvme_drives.push(PcieNvmeDrive {
840 port_name: "s0rc0rp0".into(),
841 nsid: 1,
842 drive: boot_drive,
843 });
844 self
845 }
846 }
847 } else {
848 self
849 }
850 }
851
852 fn has_agent_disk(&self) -> bool {
857 if self.uses_pipette_as_init() {
858 self.agent_image.as_ref().is_some_and(|i| i.has_extras())
859 } else {
860 self.agent_image.is_some()
861 }
862 }
863
864 pub fn properties(&self) -> PetriVmProperties {
866 PetriVmProperties {
867 is_openhcl: self.config.firmware.is_openhcl(),
868 is_isolated: self.config.firmware.isolation().is_some(),
869 is_pcat: self.config.firmware.is_pcat(),
870 is_linux_direct: self.config.firmware.is_linux_direct(),
871 using_vtl0_pipette: self.using_vtl0_pipette(),
872 using_vpci: self.boot_device_type.requires_vpci_boot(),
873 os_flavor: self.config.firmware.os_flavor(),
874 minimal_mode: self.minimal_mode,
875 uses_pipette_as_init: self.uses_pipette_as_init(),
876 enable_serial: self.enable_serial,
877 prebuilt_initrd: self.prebuilt_initrd.clone(),
878 has_agent_disk: self.has_agent_disk(),
879 }
880 }
881
882 fn uses_pipette_as_init(&self) -> bool {
888 self.config.firmware.is_linux_direct()
889 && !self.config.firmware.is_openhcl()
890 && self.pipette_binary.is_some()
891 }
892
893 pub fn using_vtl0_pipette(&self) -> bool {
895 self.uses_pipette_as_init()
896 || self
897 .agent_image
898 .as_ref()
899 .is_some_and(|x| x.contains_pipette())
900 }
901
902 pub async fn run_without_agent(self) -> anyhow::Result<PetriVm<T>> {
906 self.run_core().await
907 }
908
909 pub async fn run(self) -> anyhow::Result<(PetriVm<T>, PipetteClient)> {
912 assert!(self.using_vtl0_pipette());
913
914 let mut vm = self.run_core().await?;
915 let client = vm.wait_for_agent().await?;
916 Ok((vm, client))
917 }
918
919 async fn run_core(mut self) -> anyhow::Result<PetriVm<T>> {
920 self = self.add_boot_disk().add_agent_disks();
923
924 let _prepared_initrd_guard;
928 if self.uses_pipette_as_init() && self.prebuilt_initrd.is_none() {
929 let tmp = self.prepare_initrd()?;
930 self.prebuilt_initrd = Some(tmp.to_path_buf());
931 _prepared_initrd_guard = Some(tmp);
932 } else {
933 _prepared_initrd_guard = None;
934 }
935
936 tracing::debug!(builder = ?self);
937
938 let arch = self.config.arch;
939 let expect_reset = self.expect_reset();
940 let uses_pipette_as_init = self.uses_pipette_as_init();
941 let properties = self.properties();
942
943 let (mut runtime, config) = self
944 .backend
945 .run(
946 self.config,
947 self.modify_vmm_config,
948 &self.resources,
949 properties,
950 )
951 .await?;
952 let openhcl_diag_handler = runtime.openhcl_diag();
953 let watchdog_tasks =
954 Self::start_watchdog_tasks(&self.resources, &mut runtime, self.enable_screenshots)?;
955
956 let mut vm = PetriVm {
957 resources: self.resources,
958 runtime,
959 watchdog_tasks,
960 openhcl_diag_handler,
961
962 arch,
963 guest_quirks: self.guest_quirks,
964 vmm_quirks: self.vmm_quirks,
965 expected_boot_event: self.expected_boot_event,
966 uses_pipette_as_init,
967
968 config,
969 };
970
971 if expect_reset {
972 vm.wait_for_reset_core().await?;
973 }
974
975 vm.wait_for_expected_boot_event().await?;
976
977 Ok(vm)
978 }
979
980 fn expect_reset(&self) -> bool {
981 self.override_expect_reset
982 || matches!(
983 (
984 self.guest_quirks.initial_reboot,
985 self.expected_boot_event,
986 &self.config.firmware,
987 &self.config.tpm,
988 ),
989 (
990 Some(InitialRebootCondition::Always),
991 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
992 _,
993 _,
994 ) | (
995 Some(InitialRebootCondition::WithTpm),
996 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
997 _,
998 Some(_),
999 )
1000 )
1001 }
1002
1003 fn start_watchdog_tasks(
1004 resources: &PetriVmResources,
1005 runtime: &mut T::VmRuntime,
1006 enable_screenshots: bool,
1007 ) -> anyhow::Result<Vec<Task<()>>> {
1008 let mut tasks = Vec::new();
1009
1010 {
1011 const TIMEOUT_DURATION_MINUTES: u64 = 10;
1012 const TIMER_DURATION: Duration = Duration::from_secs(TIMEOUT_DURATION_MINUTES * 60);
1013 let log_source = resources.log_source.clone();
1014 let inspect_task =
1015 |name,
1016 driver: &DefaultDriver,
1017 inspect: std::pin::Pin<Box<dyn Future<Output = _> + Send>>| {
1018 driver.spawn(format!("petri-watchdog-inspect-{name}"), async move {
1019 if CancelContext::new()
1020 .with_timeout(Duration::from_secs(10))
1021 .until_cancelled(save_inspect(name, inspect, &log_source))
1022 .await
1023 .is_err()
1024 {
1025 tracing::warn!(name, "Failed to collect inspect data within timeout");
1026 }
1027 })
1028 };
1029
1030 let driver = resources.driver.clone();
1031 let vmm_inspector = runtime.inspector();
1032 let openhcl_diag_handler = runtime.openhcl_diag();
1033 tasks.push(resources.driver.spawn("timer-watchdog", async move {
1034 PolledTimer::new(&driver).sleep(TIMER_DURATION).await;
1035 tracing::warn!("Test timeout reached after {TIMEOUT_DURATION_MINUTES} minutes, collecting diagnostics.");
1036 let mut timeout_tasks = Vec::new();
1037 if let Some(inspector) = vmm_inspector {
1038 timeout_tasks.push(inspect_task.clone()("vmm", &driver, Box::pin(async move { inspector.inspect_all().await })) );
1039 }
1040 if let Some(openhcl_diag_handler) = openhcl_diag_handler {
1041 timeout_tasks.push(inspect_task("openhcl", &driver, Box::pin(async move { openhcl_diag_handler.inspect("", None, None).await })));
1042 }
1043 futures::future::join_all(timeout_tasks).await;
1044 tracing::error!("Test time out diagnostics collection complete, aborting.");
1045 panic!("Test timed out");
1046 }));
1047 }
1048
1049 if enable_screenshots {
1050 if let Some(mut framebuffer_access) = runtime.take_framebuffer_access() {
1051 let mut timer = PolledTimer::new(&resources.driver);
1052 let log_source = resources.log_source.clone();
1053
1054 tasks.push(
1055 resources
1056 .driver
1057 .spawn("petri-watchdog-screenshot", async move {
1058 let mut image = Vec::new();
1059 let mut last_image = Vec::new();
1060 loop {
1061 timer.sleep(Duration::from_secs(2)).await;
1062 tracing::trace!("Taking screenshot.");
1063
1064 let VmScreenshotMeta {
1065 color,
1066 width,
1067 height,
1068 } = match framebuffer_access.screenshot(&mut image).await {
1069 Ok(Some(meta)) => meta,
1070 Ok(None) => {
1071 tracing::debug!("VM off, skipping screenshot.");
1072 continue;
1073 }
1074 Err(e) => {
1075 tracing::error!(?e, "Failed to take screenshot");
1076 continue;
1077 }
1078 };
1079
1080 if image == last_image {
1081 tracing::debug!(
1082 "No change in framebuffer, skipping screenshot."
1083 );
1084 continue;
1085 }
1086
1087 let r = log_source.create_attachment("screenshot.png").and_then(
1088 |mut f| {
1089 image::write_buffer_with_format(
1090 &mut f,
1091 &image,
1092 width.into(),
1093 height.into(),
1094 color,
1095 image::ImageFormat::Png,
1096 )
1097 .map_err(Into::into)
1098 },
1099 );
1100
1101 if let Err(e) = r {
1102 tracing::error!(?e, "Failed to save screenshot");
1103 } else {
1104 tracing::info!("Screenshot saved.");
1105 }
1106
1107 std::mem::swap(&mut image, &mut last_image);
1108 }
1109 }),
1110 );
1111 }
1112 }
1113
1114 Ok(tasks)
1115 }
1116
1117 pub fn with_expect_boot_failure(mut self) -> Self {
1120 self.expected_boot_event = Some(FirmwareEvent::BootFailed);
1121 self
1122 }
1123
1124 pub fn with_expect_no_boot_event(mut self) -> Self {
1127 self.expected_boot_event = None;
1128 self
1129 }
1130
1131 pub fn with_expect_reset(mut self) -> Self {
1135 self.override_expect_reset = true;
1136 self
1137 }
1138
1139 pub fn with_secure_boot(mut self) -> Self {
1141 self.config
1142 .firmware
1143 .uefi_config_mut()
1144 .expect("Secure boot is only supported for UEFI firmware.")
1145 .secure_boot_enabled = true;
1146
1147 match self.os_flavor() {
1148 OsFlavor::Windows => self.with_windows_secure_boot_template(),
1149 OsFlavor::Linux => self.with_uefi_ca_secure_boot_template(),
1150 _ => panic!(
1151 "Secure boot unsupported for OS flavor {:?}",
1152 self.os_flavor()
1153 ),
1154 }
1155 }
1156
1157 pub fn with_windows_secure_boot_template(mut self) -> Self {
1159 self.config
1160 .firmware
1161 .uefi_config_mut()
1162 .expect("Secure boot is only supported for UEFI firmware.")
1163 .secure_boot_template = Some(SecureBootTemplate::MicrosoftWindows);
1164 self
1165 }
1166
1167 pub fn with_uefi_ca_secure_boot_template(mut self) -> Self {
1169 self.config
1170 .firmware
1171 .uefi_config_mut()
1172 .expect("Secure boot is only supported for UEFI firmware.")
1173 .secure_boot_template = Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority);
1174 self
1175 }
1176
1177 pub fn with_processor_topology(mut self, topology: ProcessorTopology) -> Self {
1179 self.config.proc_topology = topology;
1180 self
1181 }
1182
1183 pub fn with_memory(mut self, memory: MemoryConfig) -> Self {
1185 self.config.memory = memory;
1186 self
1187 }
1188
1189 pub fn with_vtl2_base_address_type(mut self, address_type: Vtl2BaseAddressType) -> Self {
1194 self.config
1195 .firmware
1196 .openhcl_config_mut()
1197 .expect("OpenHCL firmware is required to set custom VTL2 address type.")
1198 .vtl2_base_address_type = Some(address_type);
1199 self
1200 }
1201
1202 pub fn with_custom_openhcl(mut self, artifact: ResolvedArtifact<impl IsOpenhclIgvm>) -> Self {
1204 match &mut self.config.firmware {
1205 Firmware::OpenhclLinuxDirect { igvm_path, .. }
1206 | Firmware::OpenhclPcat { igvm_path, .. }
1207 | Firmware::OpenhclUefi { igvm_path, .. } => {
1208 *igvm_path = artifact.erase();
1209 }
1210 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } | Firmware::Pcat { .. } => {
1211 panic!("Custom OpenHCL is only supported for OpenHCL firmware.")
1212 }
1213 }
1214 self
1215 }
1216
1217 pub fn with_openhcl_command_line(mut self, additional_command_line: &str) -> Self {
1219 append_cmdline(
1220 &mut self
1221 .config
1222 .firmware
1223 .openhcl_config_mut()
1224 .expect("OpenHCL command line is only supported for OpenHCL firmware.")
1225 .custom_command_line,
1226 additional_command_line,
1227 );
1228 self
1229 }
1230
1231 pub fn with_confidential_filtering(self) -> Self {
1233 if !self.config.firmware.is_openhcl() {
1234 panic!("Confidential filtering is only supported for OpenHCL");
1235 }
1236 self.with_openhcl_command_line(&format!(
1237 "{}=1 {}=0",
1238 underhill_confidentiality::OPENHCL_CONFIDENTIAL_ENV_VAR_NAME,
1239 underhill_confidentiality::OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME
1240 ))
1241 }
1242
1243 pub fn with_openhcl_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1245 self.config
1246 .firmware
1247 .openhcl_config_mut()
1248 .expect("OpenHCL firmware is required to set custom OpenHCL log levels.")
1249 .log_levels = levels;
1250 self
1251 }
1252
1253 pub fn with_host_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1257 if let OpenvmmLogConfig::Custom(ref custom_levels) = levels {
1258 for key in custom_levels.keys() {
1259 if !["OPENVMM_LOG", "OPENVMM_SHOW_SPANS"].contains(&key.as_str()) {
1260 panic!("Unsupported OpenVMM log level key: {}", key);
1261 }
1262 }
1263 }
1264
1265 self.config.host_log_levels = Some(levels.clone());
1266 self
1267 }
1268
1269 pub fn with_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
1271 self.agent_image
1272 .as_mut()
1273 .expect("no guest pipette")
1274 .add_file(name, artifact);
1275 self
1276 }
1277
1278 pub fn with_openhcl_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
1280 self.openhcl_agent_image
1281 .as_mut()
1282 .expect("no openhcl pipette")
1283 .add_file(name, artifact);
1284 self
1285 }
1286
1287 pub fn with_uefi_frontpage(mut self, enable: bool) -> Self {
1289 self.config
1290 .firmware
1291 .uefi_config_mut()
1292 .expect("UEFI frontpage is only supported for UEFI firmware.")
1293 .disable_frontpage = !enable;
1294 self
1295 }
1296
1297 pub fn with_default_boot_always_attempt(mut self, enable: bool) -> Self {
1299 self.config
1300 .firmware
1301 .uefi_config_mut()
1302 .expect("Default boot always attempt is only supported for UEFI firmware.")
1303 .default_boot_always_attempt = enable;
1304 self
1305 }
1306
1307 pub fn with_vmbus_redirect(mut self, enable: bool) -> Self {
1309 self.config
1310 .firmware
1311 .openhcl_config_mut()
1312 .expect("VMBus redirection is only supported for OpenHCL firmware.")
1313 .vmbus_redirect = enable;
1314 self
1315 }
1316
1317 pub fn with_guest_state_lifetime(
1319 mut self,
1320 guest_state_lifetime: PetriGuestStateLifetime,
1321 ) -> Self {
1322 let disk = match self.config.vmgs {
1323 PetriVmgsResource::Disk(disk)
1324 | PetriVmgsResource::ReprovisionOnFailure(disk)
1325 | PetriVmgsResource::Reprovision(disk) => disk,
1326 PetriVmgsResource::Ephemeral => PetriVmgsDisk::default(),
1327 };
1328 self.config.vmgs = match guest_state_lifetime {
1329 PetriGuestStateLifetime::Disk => PetriVmgsResource::Disk(disk),
1330 PetriGuestStateLifetime::ReprovisionOnFailure => {
1331 PetriVmgsResource::ReprovisionOnFailure(disk)
1332 }
1333 PetriGuestStateLifetime::Reprovision => PetriVmgsResource::Reprovision(disk),
1334 PetriGuestStateLifetime::Ephemeral => {
1335 if !matches!(disk.disk, Disk::Memory(_)) {
1336 panic!("attempted to use ephemeral guest state after specifying backing vmgs")
1337 }
1338 PetriVmgsResource::Ephemeral
1339 }
1340 };
1341 self
1342 }
1343
1344 pub fn with_guest_state_encryption(mut self, policy: GuestStateEncryptionPolicy) -> Self {
1346 match &mut self.config.vmgs {
1347 PetriVmgsResource::Disk(vmgs)
1348 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1349 | PetriVmgsResource::Reprovision(vmgs) => {
1350 vmgs.encryption_policy = policy;
1351 }
1352 PetriVmgsResource::Ephemeral => {
1353 panic!("attempted to encrypt ephemeral guest state")
1354 }
1355 }
1356 self
1357 }
1358
1359 pub fn with_initial_vmgs(self, disk: ResolvedArtifact<impl IsTestVmgs>) -> Self {
1361 self.with_backing_vmgs(Disk::Differencing(DiskPath::Local(disk.into())))
1362 }
1363
1364 pub fn with_persistent_vmgs(self, disk: impl AsRef<Path>) -> Self {
1366 self.with_backing_vmgs(Disk::Persistent(disk.as_ref().to_path_buf()))
1367 }
1368
1369 fn with_backing_vmgs(mut self, disk: Disk) -> Self {
1370 match &mut self.config.vmgs {
1371 PetriVmgsResource::Disk(vmgs)
1372 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1373 | PetriVmgsResource::Reprovision(vmgs) => {
1374 if !matches!(vmgs.disk, Disk::Memory(_)) {
1375 panic!("already specified a backing vmgs file");
1376 }
1377 vmgs.disk = disk;
1378 }
1379 PetriVmgsResource::Ephemeral => {
1380 panic!("attempted to specify a backing vmgs with ephemeral guest state")
1381 }
1382 }
1383 self
1384 }
1385
1386 pub fn with_boot_device_type(mut self, boot: BootDeviceType) -> Self {
1390 self.boot_device_type = boot;
1391 self
1392 }
1393
1394 pub fn with_tpm(mut self, enable: bool) -> Self {
1396 if enable {
1397 self.config.tpm.get_or_insert_default();
1398 } else {
1399 self.config.tpm = None;
1400 }
1401 self
1402 }
1403
1404 pub fn with_tpm_state_persistence(mut self, tpm_state_persistence: bool) -> Self {
1406 self.config
1407 .tpm
1408 .as_mut()
1409 .expect("TPM persistence requires a TPM")
1410 .no_persistent_secrets = !tpm_state_persistence;
1411 self
1412 }
1413
1414 pub fn with_custom_vtl2_settings(
1418 mut self,
1419 f: impl FnOnce(&mut Vtl2Settings) + 'static + Send + Sync,
1420 ) -> Self {
1421 f(self
1422 .config
1423 .firmware
1424 .vtl2_settings()
1425 .expect("Custom VTL 2 settings are only supported with OpenHCL"));
1426 self
1427 }
1428
1429 pub fn add_vtl2_storage_controller(self, controller: StorageController) -> Self {
1431 self.with_custom_vtl2_settings(move |v| {
1432 v.dynamic
1433 .as_mut()
1434 .unwrap()
1435 .storage_controllers
1436 .push(controller)
1437 })
1438 }
1439
1440 pub fn add_vmbus_storage_controller(
1442 mut self,
1443 id: &Guid,
1444 target_vtl: Vtl,
1445 controller_type: VmbusStorageType,
1446 ) -> Self {
1447 if self
1448 .config
1449 .vmbus_storage_controllers
1450 .insert(
1451 *id,
1452 VmbusStorageController::new(target_vtl, controller_type),
1453 )
1454 .is_some()
1455 {
1456 panic!("storage controller {id} already existed");
1457 }
1458 self
1459 }
1460
1461 pub fn add_vmbus_drive(
1463 mut self,
1464 drive: Drive,
1465 controller_id: &Guid,
1466 controller_location: Option<u32>,
1467 ) -> Self {
1468 let controller = self
1469 .config
1470 .vmbus_storage_controllers
1471 .get_mut(controller_id)
1472 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
1473
1474 _ = controller.set_drive(controller_location, drive, false);
1475
1476 self
1477 }
1478
1479 pub fn add_ide_drive(
1481 mut self,
1482 drive: Drive,
1483 controller_number: u32,
1484 controller_location: u8,
1485 ) -> Self {
1486 self.config
1487 .firmware
1488 .ide_controllers_mut()
1489 .expect("Host IDE requires PCAT with no HCL")[controller_number as usize]
1490 [controller_location as usize] = Some(drive);
1491
1492 self
1493 }
1494
1495 pub fn os_flavor(&self) -> OsFlavor {
1497 self.config.firmware.os_flavor()
1498 }
1499
1500 pub fn is_openhcl(&self) -> bool {
1502 self.config.firmware.is_openhcl()
1503 }
1504
1505 pub fn isolation(&self) -> Option<IsolationType> {
1507 self.config.firmware.isolation()
1508 }
1509
1510 pub fn arch(&self) -> MachineArch {
1512 self.config.arch
1513 }
1514
1515 pub fn log_source(&self) -> &PetriLogSource {
1517 &self.resources.log_source
1518 }
1519
1520 pub fn default_servicing_flags(&self) -> OpenHclServicingFlags {
1522 T::default_servicing_flags()
1523 }
1524
1525 pub fn modify_backend(
1527 mut self,
1528 f: impl FnOnce(T::VmmConfig) -> T::VmmConfig + 'static + Send,
1529 ) -> Self {
1530 if self.modify_vmm_config.is_some() {
1531 panic!("only one modify_backend allowed");
1532 }
1533 self.modify_vmm_config = Some(ModifyFn(Box::new(f)));
1534 self
1535 }
1536}
1537
1538impl<T: PetriVmmBackend> PetriVm<T> {
1539 pub async fn teardown(self) -> anyhow::Result<()> {
1541 tracing::info!("Tearing down VM...");
1542 self.runtime.teardown().await
1543 }
1544
1545 pub async fn wait_for_halt(&mut self) -> anyhow::Result<PetriHaltReason> {
1547 tracing::info!("Waiting for VM to halt...");
1548 let halt_reason = self.runtime.wait_for_halt(false).await?;
1549 tracing::info!("VM halted: {halt_reason:?}. Cancelling watchdogs...");
1550 futures::future::join_all(self.watchdog_tasks.drain(..).map(|t| t.cancel())).await;
1551 Ok(halt_reason)
1552 }
1553
1554 pub async fn wait_for_clean_shutdown(&mut self) -> anyhow::Result<()> {
1556 let halt_reason = self.wait_for_halt().await?;
1557 if halt_reason != PetriHaltReason::PowerOff {
1558 anyhow::bail!("Expected PowerOff, got {halt_reason:?}");
1559 }
1560 tracing::info!("VM was cleanly powered off and torn down.");
1561 Ok(())
1562 }
1563
1564 pub async fn wait_for_teardown(mut self) -> anyhow::Result<PetriHaltReason> {
1567 let halt_reason = self.wait_for_halt().await?;
1568 self.teardown().await?;
1569 Ok(halt_reason)
1570 }
1571
1572 pub async fn wait_for_clean_teardown(mut self) -> anyhow::Result<()> {
1574 self.wait_for_clean_shutdown().await?;
1575 self.teardown().await
1576 }
1577
1578 pub async fn wait_for_reset_no_agent(&mut self) -> anyhow::Result<()> {
1580 self.wait_for_reset_core().await?;
1581 self.wait_for_expected_boot_event().await?;
1582 Ok(())
1583 }
1584
1585 pub async fn wait_for_reset(&mut self) -> anyhow::Result<PipetteClient> {
1587 self.wait_for_reset_no_agent().await?;
1588 self.wait_for_agent().await
1589 }
1590
1591 async fn wait_for_reset_core(&mut self) -> anyhow::Result<()> {
1592 tracing::info!("Waiting for VM to reset...");
1593 let halt_reason = self.runtime.wait_for_halt(true).await?;
1594 if halt_reason != PetriHaltReason::Reset {
1595 anyhow::bail!("Expected reset, got {halt_reason:?}");
1596 }
1597 tracing::info!("VM reset.");
1598 Ok(())
1599 }
1600
1601 pub async fn inspect_openhcl(
1612 &self,
1613 path: impl Into<String>,
1614 depth: Option<usize>,
1615 timeout: Option<Duration>,
1616 ) -> anyhow::Result<inspect::Node> {
1617 self.openhcl_diag()?
1618 .inspect(path.into().as_str(), depth, timeout)
1619 .await
1620 }
1621
1622 pub async fn inspect_update_openhcl(
1632 &self,
1633 path: impl Into<String>,
1634 value: impl Into<String>,
1635 ) -> anyhow::Result<inspect::Value> {
1636 self.openhcl_diag()?
1637 .inspect_update(path.into(), value.into())
1638 .await
1639 }
1640
1641 pub async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()> {
1643 self.inspect_openhcl("", None, None).await.map(|_| ())
1644 }
1645
1646 pub async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()> {
1652 self.openhcl_diag()?.wait_for_vtl2().await
1653 }
1654
1655 pub async fn kmsg(&self) -> anyhow::Result<diag_client::kmsg_stream::KmsgStream> {
1657 self.openhcl_diag()?.kmsg().await
1658 }
1659
1660 pub async fn openhcl_core_dump(&self, name: &str, path: &Path) -> anyhow::Result<()> {
1663 self.openhcl_diag()?.core_dump(name, path).await
1664 }
1665
1666 pub async fn openhcl_crash(&self, name: &str) -> anyhow::Result<()> {
1668 self.openhcl_diag()?.crash(name).await
1669 }
1670
1671 async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient> {
1674 if !self.uses_pipette_as_init {
1684 self.runtime.wait_for_enlightened_shutdown_ready().await?;
1685 }
1686 self.runtime.wait_for_agent(false).await
1687 }
1688
1689 pub async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient> {
1693 self.launch_vtl2_pipette().await?;
1695 self.runtime.wait_for_agent(true).await
1696 }
1697
1698 async fn wait_for_expected_boot_event(&mut self) -> anyhow::Result<()> {
1705 if let Some(expected_event) = self.expected_boot_event {
1706 let event = self.wait_for_boot_event().await?;
1707
1708 anyhow::ensure!(
1709 event == expected_event,
1710 "Did not receive expected boot event"
1711 );
1712 } else {
1713 tracing::warn!("Boot event not emitted for configured firmware or manually ignored.");
1714 }
1715
1716 Ok(())
1717 }
1718
1719 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
1722 tracing::info!("Waiting for boot event...");
1723 let boot_event = loop {
1724 match CancelContext::new()
1725 .with_timeout(self.vmm_quirks.flaky_boot.unwrap_or(Duration::MAX))
1726 .until_cancelled(self.runtime.wait_for_boot_event())
1727 .await
1728 {
1729 Ok(res) => break res?,
1730 Err(_) => {
1731 tracing::error!("Did not get boot event in required time, resetting...");
1732 if let Some(inspector) = self.runtime.inspector() {
1733 save_inspect(
1734 "vmm",
1735 Box::pin(async move { inspector.inspect_all().await }),
1736 &self.resources.log_source,
1737 )
1738 .await;
1739 }
1740
1741 self.runtime.reset().await?;
1742 continue;
1743 }
1744 }
1745 };
1746 tracing::info!("Got boot event: {boot_event:?}");
1747 Ok(boot_event)
1748 }
1749
1750 pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
1753 tracing::info!("Waiting for enlightened shutdown to be ready");
1754 self.runtime.wait_for_enlightened_shutdown_ready().await?;
1755
1756 let mut wait_time = Duration::from_secs(10);
1762
1763 if let Some(duration) = self.guest_quirks.hyperv_shutdown_ic_sleep {
1765 wait_time += duration;
1766 }
1767
1768 tracing::info!(
1769 "Shutdown IC reported ready, waiting for an extra {}s",
1770 wait_time.as_secs()
1771 );
1772 PolledTimer::new(&self.resources.driver)
1773 .sleep(wait_time)
1774 .await;
1775
1776 tracing::info!("Sending enlightened shutdown command");
1777 self.runtime.send_enlightened_shutdown(kind).await
1778 }
1779
1780 pub async fn restart_openhcl(
1783 &mut self,
1784 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1785 flags: OpenHclServicingFlags,
1786 ) -> anyhow::Result<()> {
1787 self.runtime
1788 .restart_openhcl(&new_openhcl.erase(), flags)
1789 .await
1790 }
1791
1792 pub async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()> {
1795 self.runtime.update_command_line(command_line).await
1796 }
1797
1798 pub async fn add_pcie_device(
1800 &mut self,
1801 port_name: String,
1802 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
1803 ) -> anyhow::Result<()> {
1804 self.runtime.add_pcie_device(port_name, resource).await
1805 }
1806
1807 pub async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
1809 self.runtime.remove_pcie_device(port_name).await
1810 }
1811
1812 pub async fn save_openhcl(
1815 &mut self,
1816 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1817 flags: OpenHclServicingFlags,
1818 ) -> anyhow::Result<()> {
1819 self.runtime.save_openhcl(&new_openhcl.erase(), flags).await
1820 }
1821
1822 pub async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
1825 self.runtime.restore_openhcl().await
1826 }
1827
1828 pub fn arch(&self) -> MachineArch {
1830 self.arch
1831 }
1832
1833 pub fn backend(&mut self) -> &mut T::VmRuntime {
1835 &mut self.runtime
1836 }
1837
1838 async fn launch_vtl2_pipette(&self) -> anyhow::Result<()> {
1839 tracing::debug!("Launching VTL 2 pipette...");
1840
1841 let res = self
1843 .openhcl_diag()?
1844 .run_vtl2_command("sh", &["-c", "mkdir /cidata && mount LABEL=cidata /cidata"])
1845 .await?;
1846
1847 if !res.exit_status.success() {
1848 anyhow::bail!("Failed to mount VTL 2 pipette drive: {:?}", res);
1849 }
1850
1851 let res = self
1852 .openhcl_diag()?
1853 .run_detached_vtl2_command("sh", &["-c", "/cidata/pipette 2>&1 | logger &"])
1854 .await?;
1855
1856 if !res.success() {
1857 anyhow::bail!("Failed to spawn VTL 2 pipette: {:?}", res);
1858 }
1859
1860 Ok(())
1861 }
1862
1863 fn openhcl_diag(&self) -> anyhow::Result<&OpenHclDiagHandler> {
1864 if let Some(ohd) = self.openhcl_diag_handler.as_ref() {
1865 Ok(ohd)
1866 } else {
1867 anyhow::bail!("VM is not configured with OpenHCL")
1868 }
1869 }
1870
1871 pub async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
1873 self.runtime.get_guest_state_file().await
1874 }
1875
1876 pub async fn modify_vtl2_settings(
1878 &mut self,
1879 f: impl FnOnce(&mut Vtl2Settings),
1880 ) -> anyhow::Result<()> {
1881 if self.openhcl_diag_handler.is_none() {
1882 panic!("Custom VTL 2 settings are only supported with OpenHCL");
1883 }
1884 f(self
1885 .config
1886 .vtl2_settings
1887 .get_or_insert_with(default_vtl2_settings));
1888 self.runtime
1889 .set_vtl2_settings(self.config.vtl2_settings.as_ref().unwrap())
1890 .await
1891 }
1892
1893 pub fn get_vmbus_storage_controllers(&self) -> &HashMap<Guid, VmbusStorageController> {
1895 &self.config.vmbus_storage_controllers
1896 }
1897
1898 pub async fn set_vmbus_drive(
1900 &mut self,
1901 drive: Drive,
1902 controller_id: &Guid,
1903 controller_location: Option<u32>,
1904 ) -> anyhow::Result<()> {
1905 let controller = self
1906 .config
1907 .vmbus_storage_controllers
1908 .get_mut(controller_id)
1909 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
1910
1911 let controller_location = controller.set_drive(controller_location, drive, true);
1912 let disk = controller.drives.get(&controller_location).unwrap();
1913
1914 self.runtime
1915 .set_vmbus_drive(disk, controller_id, controller_location)
1916 .await?;
1917
1918 Ok(())
1919 }
1920}
1921
1922#[async_trait]
1924pub trait PetriVmRuntime: Send + Sync + 'static {
1925 type VmInspector: PetriVmInspector;
1927 type VmFramebufferAccess: PetriVmFramebufferAccess;
1929
1930 async fn teardown(self) -> anyhow::Result<()>;
1932 async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReason>;
1935 async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>;
1937 fn openhcl_diag(&self) -> Option<OpenHclDiagHandler>;
1939 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent>;
1942 async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()>;
1945 async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
1947 async fn restart_openhcl(
1950 &mut self,
1951 new_openhcl: &ResolvedArtifact,
1952 flags: OpenHclServicingFlags,
1953 ) -> anyhow::Result<()>;
1954 async fn save_openhcl(
1958 &mut self,
1959 new_openhcl: &ResolvedArtifact,
1960 flags: OpenHclServicingFlags,
1961 ) -> anyhow::Result<()>;
1962 async fn restore_openhcl(&mut self) -> anyhow::Result<()>;
1965 async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()>;
1968 fn inspector(&self) -> Option<Self::VmInspector> {
1970 None
1971 }
1972 fn take_framebuffer_access(&mut self) -> Option<Self::VmFramebufferAccess> {
1975 None
1976 }
1977 async fn reset(&mut self) -> anyhow::Result<()>;
1979 async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
1981 Ok(None)
1982 }
1983 async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()>;
1985 async fn set_vmbus_drive(
1987 &mut self,
1988 disk: &Drive,
1989 controller_id: &Guid,
1990 controller_location: u32,
1991 ) -> anyhow::Result<()>;
1992 async fn add_pcie_device(
1994 &mut self,
1995 port_name: String,
1996 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
1997 ) -> anyhow::Result<()> {
1998 let _ = (port_name, resource);
1999 anyhow::bail!("PCIe hotplug not supported by this backend")
2000 }
2001 async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
2003 let _ = port_name;
2004 anyhow::bail!("PCIe hotplug not supported by this backend")
2005 }
2006}
2007
2008#[async_trait]
2010pub trait PetriVmInspector: Send + Sync + 'static {
2011 async fn inspect_all(&self) -> anyhow::Result<inspect::Node>;
2013}
2014
2015pub struct NoPetriVmInspector;
2017#[async_trait]
2018impl PetriVmInspector for NoPetriVmInspector {
2019 async fn inspect_all(&self) -> anyhow::Result<inspect::Node> {
2020 unreachable!()
2021 }
2022}
2023
2024pub struct VmScreenshotMeta {
2026 pub color: image::ExtendedColorType,
2028 pub width: u16,
2030 pub height: u16,
2032}
2033
2034#[async_trait]
2036pub trait PetriVmFramebufferAccess: Send + 'static {
2037 async fn screenshot(&mut self, image: &mut Vec<u8>)
2040 -> anyhow::Result<Option<VmScreenshotMeta>>;
2041}
2042
2043pub struct NoPetriVmFramebufferAccess;
2045#[async_trait]
2046impl PetriVmFramebufferAccess for NoPetriVmFramebufferAccess {
2047 async fn screenshot(
2048 &mut self,
2049 _image: &mut Vec<u8>,
2050 ) -> anyhow::Result<Option<VmScreenshotMeta>> {
2051 unreachable!()
2052 }
2053}
2054
2055#[derive(Debug)]
2057pub struct ProcessorTopology {
2058 pub vp_count: u32,
2060 pub enable_smt: Option<bool>,
2062 pub vps_per_socket: Option<u32>,
2064 pub apic_mode: Option<ApicMode>,
2066}
2067
2068impl Default for ProcessorTopology {
2069 fn default() -> Self {
2070 Self {
2071 vp_count: 2,
2072 enable_smt: None,
2073 vps_per_socket: None,
2074 apic_mode: None,
2075 }
2076 }
2077}
2078
2079impl ProcessorTopology {
2080 pub fn heavy() -> Self {
2082 Self {
2083 vp_count: 16,
2084 vps_per_socket: Some(8),
2085 ..Default::default()
2086 }
2087 }
2088
2089 pub fn very_heavy() -> Self {
2091 Self {
2092 vp_count: 32,
2093 vps_per_socket: Some(16),
2094 ..Default::default()
2095 }
2096 }
2097}
2098
2099#[derive(Debug, Clone, Copy)]
2101pub enum ApicMode {
2102 Xapic,
2104 X2apicSupported,
2106 X2apicEnabled,
2108}
2109
2110#[derive(Debug)]
2112pub enum MmioConfig {
2113 Platform,
2115 Custom(Vec<MemoryRange>),
2118}
2119
2120#[derive(Debug)]
2122pub struct MemoryConfig {
2123 pub startup_bytes: u64,
2126 pub dynamic_memory_range: Option<(u64, u64)>,
2130 pub mmio_gaps: MmioConfig,
2132}
2133
2134impl Default for MemoryConfig {
2135 fn default() -> Self {
2136 Self {
2137 startup_bytes: 4 * 1024 * 1024 * 1024, dynamic_memory_range: None,
2139 mmio_gaps: MmioConfig::Platform,
2140 }
2141 }
2142}
2143
2144#[derive(Debug)]
2146pub struct UefiConfig {
2147 pub secure_boot_enabled: bool,
2149 pub secure_boot_template: Option<SecureBootTemplate>,
2151 pub disable_frontpage: bool,
2153 pub default_boot_always_attempt: bool,
2155 pub enable_vpci_boot: bool,
2157}
2158
2159impl Default for UefiConfig {
2160 fn default() -> Self {
2161 Self {
2162 secure_boot_enabled: false,
2163 secure_boot_template: None,
2164 disable_frontpage: true,
2165 default_boot_always_attempt: false,
2166 enable_vpci_boot: false,
2167 }
2168 }
2169}
2170
2171#[derive(Debug, Clone)]
2173pub enum OpenvmmLogConfig {
2174 TestDefault,
2178 BuiltInDefault,
2181 Custom(BTreeMap<String, String>),
2191}
2192
2193#[derive(Debug)]
2195pub struct OpenHclConfig {
2196 pub vmbus_redirect: bool,
2198 pub custom_command_line: Option<String>,
2202 pub log_levels: OpenvmmLogConfig,
2206 pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
2209 pub vtl2_settings: Option<Vtl2Settings>,
2211}
2212
2213impl OpenHclConfig {
2214 pub fn command_line(&self) -> String {
2217 let mut cmdline = self.custom_command_line.clone();
2218
2219 append_cmdline(&mut cmdline, "OPENHCL_MANA_KEEP_ALIVE=host,privatepool");
2221
2222 match &self.log_levels {
2223 OpenvmmLogConfig::TestDefault => {
2224 let default_log_levels = {
2225 let openhcl_tracing = if let Ok(x) =
2227 std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG"))
2228 {
2229 format!("OPENVMM_LOG={x}")
2230 } else {
2231 "OPENVMM_LOG=debug".to_owned()
2232 };
2233 let openhcl_show_spans = if let Ok(x) = std::env::var("OPENVMM_SHOW_SPANS") {
2234 format!("OPENVMM_SHOW_SPANS={x}")
2235 } else {
2236 "OPENVMM_SHOW_SPANS=true".to_owned()
2237 };
2238 format!("{openhcl_tracing} {openhcl_show_spans}")
2239 };
2240 append_cmdline(&mut cmdline, &default_log_levels);
2241 }
2242 OpenvmmLogConfig::BuiltInDefault => {
2243 }
2245 OpenvmmLogConfig::Custom(levels) => {
2246 levels.iter().for_each(|(key, value)| {
2247 append_cmdline(&mut cmdline, format!("{key}={value}"));
2248 });
2249 }
2250 }
2251
2252 cmdline.unwrap_or_default()
2253 }
2254}
2255
2256impl Default for OpenHclConfig {
2257 fn default() -> Self {
2258 Self {
2259 vmbus_redirect: false,
2260 custom_command_line: None,
2261 log_levels: OpenvmmLogConfig::TestDefault,
2262 vtl2_base_address_type: None,
2263 vtl2_settings: None,
2264 }
2265 }
2266}
2267
2268#[derive(Debug)]
2270pub struct TpmConfig {
2271 pub no_persistent_secrets: bool,
2273}
2274
2275impl Default for TpmConfig {
2276 fn default() -> Self {
2277 Self {
2278 no_persistent_secrets: true,
2279 }
2280 }
2281}
2282
2283#[derive(Debug)]
2287pub enum Firmware {
2288 LinuxDirect {
2290 kernel: ResolvedArtifact,
2292 initrd: ResolvedArtifact,
2294 },
2295 OpenhclLinuxDirect {
2297 igvm_path: ResolvedArtifact,
2299 openhcl_config: OpenHclConfig,
2301 },
2302 Pcat {
2304 guest: PcatGuest,
2306 bios_firmware: ResolvedOptionalArtifact,
2308 svga_firmware: ResolvedOptionalArtifact,
2310 ide_controllers: [[Option<Drive>; 2]; 2],
2312 },
2313 OpenhclPcat {
2315 guest: PcatGuest,
2317 igvm_path: ResolvedArtifact,
2319 bios_firmware: ResolvedOptionalArtifact,
2321 svga_firmware: ResolvedOptionalArtifact,
2323 openhcl_config: OpenHclConfig,
2325 },
2326 Uefi {
2328 guest: UefiGuest,
2330 uefi_firmware: ResolvedArtifact,
2332 uefi_config: UefiConfig,
2334 },
2335 OpenhclUefi {
2337 guest: UefiGuest,
2339 isolation: Option<IsolationType>,
2341 igvm_path: ResolvedArtifact,
2343 uefi_config: UefiConfig,
2345 openhcl_config: OpenHclConfig,
2347 },
2348}
2349
2350#[derive(Debug, Copy, Clone, PartialEq, Eq)]
2352pub enum BootDeviceType {
2353 None,
2355 Ide,
2357 IdeViaScsi,
2359 IdeViaNvme,
2361 Scsi,
2363 ScsiViaScsi,
2365 ScsiViaNvme,
2367 Nvme,
2369 NvmeViaScsi,
2371 NvmeViaNvme,
2373 PcieNvme,
2375}
2376
2377impl BootDeviceType {
2378 fn requires_vtl2(&self) -> bool {
2379 match self {
2380 BootDeviceType::None
2381 | BootDeviceType::Ide
2382 | BootDeviceType::Scsi
2383 | BootDeviceType::Nvme
2384 | BootDeviceType::PcieNvme => false,
2385 BootDeviceType::IdeViaScsi
2386 | BootDeviceType::IdeViaNvme
2387 | BootDeviceType::ScsiViaScsi
2388 | BootDeviceType::ScsiViaNvme
2389 | BootDeviceType::NvmeViaScsi
2390 | BootDeviceType::NvmeViaNvme => true,
2391 }
2392 }
2393
2394 fn requires_vpci_boot(&self) -> bool {
2395 matches!(
2396 self,
2397 BootDeviceType::Nvme | BootDeviceType::NvmeViaScsi | BootDeviceType::NvmeViaNvme
2398 )
2399 }
2400}
2401
2402impl Firmware {
2403 pub fn linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2405 use petri_artifacts_vmm_test::artifacts::loadable::*;
2406 match arch {
2407 MachineArch::X86_64 => Firmware::LinuxDirect {
2408 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_X64).erase(),
2409 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
2410 },
2411 MachineArch::Aarch64 => Firmware::LinuxDirect {
2412 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_AARCH64).erase(),
2413 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_AARCH64).erase(),
2414 },
2415 }
2416 }
2417
2418 pub fn openhcl_linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2420 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2421 match arch {
2422 MachineArch::X86_64 => Firmware::OpenhclLinuxDirect {
2423 igvm_path: resolver.require(LATEST_LINUX_DIRECT_TEST_X64).erase(),
2424 openhcl_config: Default::default(),
2425 },
2426 MachineArch::Aarch64 => todo!("Linux direct not yet supported on aarch64"),
2427 }
2428 }
2429
2430 pub fn pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2432 use petri_artifacts_vmm_test::artifacts::loadable::*;
2433 Firmware::Pcat {
2434 guest,
2435 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2436 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2437 ide_controllers: [[None, None], [None, None]],
2438 }
2439 }
2440
2441 pub fn openhcl_pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2443 use petri_artifacts_vmm_test::artifacts::loadable::*;
2444 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2445 Firmware::OpenhclPcat {
2446 guest,
2447 igvm_path: resolver.require(LATEST_STANDARD_X64).erase(),
2448 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2449 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2450 openhcl_config: OpenHclConfig {
2451 vmbus_redirect: true,
2453 ..Default::default()
2454 },
2455 }
2456 }
2457
2458 pub fn uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch, guest: UefiGuest) -> Self {
2460 use petri_artifacts_vmm_test::artifacts::loadable::*;
2461 let uefi_firmware = match arch {
2462 MachineArch::X86_64 => resolver.require(UEFI_FIRMWARE_X64).erase(),
2463 MachineArch::Aarch64 => resolver.require(UEFI_FIRMWARE_AARCH64).erase(),
2464 };
2465 Firmware::Uefi {
2466 guest,
2467 uefi_firmware,
2468 uefi_config: Default::default(),
2469 }
2470 }
2471
2472 pub fn openhcl_uefi(
2474 resolver: &ArtifactResolver<'_>,
2475 arch: MachineArch,
2476 guest: UefiGuest,
2477 isolation: Option<IsolationType>,
2478 ) -> Self {
2479 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2480 let igvm_path = match arch {
2481 MachineArch::X86_64 if isolation.is_some() => resolver.require(LATEST_CVM_X64).erase(),
2482 MachineArch::X86_64 => resolver.require(LATEST_STANDARD_X64).erase(),
2483 MachineArch::Aarch64 => resolver.require(LATEST_STANDARD_AARCH64).erase(),
2484 };
2485 Firmware::OpenhclUefi {
2486 guest,
2487 isolation,
2488 igvm_path,
2489 uefi_config: Default::default(),
2490 openhcl_config: Default::default(),
2491 }
2492 }
2493
2494 fn is_openhcl(&self) -> bool {
2495 match self {
2496 Firmware::OpenhclLinuxDirect { .. }
2497 | Firmware::OpenhclUefi { .. }
2498 | Firmware::OpenhclPcat { .. } => true,
2499 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => false,
2500 }
2501 }
2502
2503 fn isolation(&self) -> Option<IsolationType> {
2504 match self {
2505 Firmware::OpenhclUefi { isolation, .. } => *isolation,
2506 Firmware::LinuxDirect { .. }
2507 | Firmware::Pcat { .. }
2508 | Firmware::Uefi { .. }
2509 | Firmware::OpenhclLinuxDirect { .. }
2510 | Firmware::OpenhclPcat { .. } => None,
2511 }
2512 }
2513
2514 fn is_linux_direct(&self) -> bool {
2515 match self {
2516 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => true,
2517 Firmware::Pcat { .. }
2518 | Firmware::Uefi { .. }
2519 | Firmware::OpenhclUefi { .. }
2520 | Firmware::OpenhclPcat { .. } => false,
2521 }
2522 }
2523
2524 pub fn linux_direct_initrd(&self) -> Option<&Path> {
2526 match self {
2527 Firmware::LinuxDirect { initrd, .. } => Some(initrd.get()),
2528 _ => None,
2529 }
2530 }
2531
2532 fn is_pcat(&self) -> bool {
2533 match self {
2534 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => true,
2535 Firmware::Uefi { .. }
2536 | Firmware::OpenhclUefi { .. }
2537 | Firmware::LinuxDirect { .. }
2538 | Firmware::OpenhclLinuxDirect { .. } => false,
2539 }
2540 }
2541
2542 fn os_flavor(&self) -> OsFlavor {
2543 match self {
2544 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => OsFlavor::Linux,
2545 Firmware::Uefi {
2546 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2547 ..
2548 }
2549 | Firmware::OpenhclUefi {
2550 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2551 ..
2552 } => OsFlavor::Uefi,
2553 Firmware::Pcat {
2554 guest: PcatGuest::Vhd(cfg),
2555 ..
2556 }
2557 | Firmware::OpenhclPcat {
2558 guest: PcatGuest::Vhd(cfg),
2559 ..
2560 }
2561 | Firmware::Uefi {
2562 guest: UefiGuest::Vhd(cfg),
2563 ..
2564 }
2565 | Firmware::OpenhclUefi {
2566 guest: UefiGuest::Vhd(cfg),
2567 ..
2568 } => cfg.os_flavor,
2569 Firmware::Pcat {
2570 guest: PcatGuest::Iso(cfg),
2571 ..
2572 }
2573 | Firmware::OpenhclPcat {
2574 guest: PcatGuest::Iso(cfg),
2575 ..
2576 } => cfg.os_flavor,
2577 }
2578 }
2579
2580 fn quirks(&self) -> GuestQuirks {
2581 match self {
2582 Firmware::Pcat {
2583 guest: PcatGuest::Vhd(cfg),
2584 ..
2585 }
2586 | Firmware::Uefi {
2587 guest: UefiGuest::Vhd(cfg),
2588 ..
2589 }
2590 | Firmware::OpenhclUefi {
2591 guest: UefiGuest::Vhd(cfg),
2592 ..
2593 } => cfg.quirks.clone(),
2594 Firmware::Pcat {
2595 guest: PcatGuest::Iso(cfg),
2596 ..
2597 } => cfg.quirks.clone(),
2598 _ => Default::default(),
2599 }
2600 }
2601
2602 fn expected_boot_event(&self) -> Option<FirmwareEvent> {
2603 match self {
2604 Firmware::LinuxDirect { .. }
2605 | Firmware::OpenhclLinuxDirect { .. }
2606 | Firmware::Uefi {
2607 guest: UefiGuest::GuestTestUefi(_),
2608 ..
2609 }
2610 | Firmware::OpenhclUefi {
2611 guest: UefiGuest::GuestTestUefi(_),
2612 ..
2613 } => None,
2614 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => {
2615 Some(FirmwareEvent::BootAttempt)
2617 }
2618 Firmware::Uefi {
2619 guest: UefiGuest::None,
2620 ..
2621 }
2622 | Firmware::OpenhclUefi {
2623 guest: UefiGuest::None,
2624 ..
2625 } => Some(FirmwareEvent::NoBootDevice),
2626 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => {
2627 Some(FirmwareEvent::BootSuccess)
2628 }
2629 }
2630 }
2631
2632 fn openhcl_config(&self) -> Option<&OpenHclConfig> {
2633 match self {
2634 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2635 | Firmware::OpenhclUefi { openhcl_config, .. }
2636 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
2637 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2638 }
2639 }
2640
2641 fn openhcl_config_mut(&mut self) -> Option<&mut OpenHclConfig> {
2642 match self {
2643 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2644 | Firmware::OpenhclUefi { openhcl_config, .. }
2645 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
2646 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2647 }
2648 }
2649
2650 #[cfg_attr(not(windows), expect(dead_code))]
2651 fn openhcl_firmware(&self) -> Option<&Path> {
2652 match self {
2653 Firmware::OpenhclLinuxDirect { igvm_path, .. }
2654 | Firmware::OpenhclUefi { igvm_path, .. }
2655 | Firmware::OpenhclPcat { igvm_path, .. } => Some(igvm_path.get()),
2656 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2657 }
2658 }
2659
2660 fn into_runtime_config(
2661 self,
2662 vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
2663 ) -> PetriVmRuntimeConfig {
2664 match self {
2665 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2666 | Firmware::OpenhclUefi { openhcl_config, .. }
2667 | Firmware::OpenhclPcat { openhcl_config, .. } => PetriVmRuntimeConfig {
2668 vtl2_settings: Some(
2669 openhcl_config
2670 .vtl2_settings
2671 .unwrap_or_else(default_vtl2_settings),
2672 ),
2673 ide_controllers: None,
2674 vmbus_storage_controllers,
2675 },
2676 Firmware::Pcat {
2677 ide_controllers, ..
2678 } => PetriVmRuntimeConfig {
2679 vtl2_settings: None,
2680 ide_controllers: Some(ide_controllers),
2681 vmbus_storage_controllers,
2682 },
2683 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } => PetriVmRuntimeConfig {
2684 vtl2_settings: None,
2685 ide_controllers: None,
2686 vmbus_storage_controllers,
2687 },
2688 }
2689 }
2690
2691 fn uefi_config(&self) -> Option<&UefiConfig> {
2692 match self {
2693 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
2694 Some(uefi_config)
2695 }
2696 Firmware::LinuxDirect { .. }
2697 | Firmware::OpenhclLinuxDirect { .. }
2698 | Firmware::Pcat { .. }
2699 | Firmware::OpenhclPcat { .. } => None,
2700 }
2701 }
2702
2703 fn uefi_config_mut(&mut self) -> Option<&mut UefiConfig> {
2704 match self {
2705 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
2706 Some(uefi_config)
2707 }
2708 Firmware::LinuxDirect { .. }
2709 | Firmware::OpenhclLinuxDirect { .. }
2710 | Firmware::Pcat { .. }
2711 | Firmware::OpenhclPcat { .. } => None,
2712 }
2713 }
2714
2715 fn boot_drive(&self) -> Option<Drive> {
2716 match self {
2717 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => None,
2718 Firmware::Pcat { guest, .. } | Firmware::OpenhclPcat { guest, .. } => {
2719 Some((guest.disk_path(), guest.is_dvd()))
2720 }
2721 Firmware::Uefi { guest, .. } | Firmware::OpenhclUefi { guest, .. } => {
2722 guest.disk_path().map(|dp| (dp, false))
2723 }
2724 }
2725 .map(|(disk_path, is_dvd)| Drive::new(Some(Disk::Differencing(disk_path)), is_dvd))
2726 }
2727
2728 fn vtl2_settings(&mut self) -> Option<&mut Vtl2Settings> {
2729 self.openhcl_config_mut()
2730 .map(|c| c.vtl2_settings.get_or_insert_with(default_vtl2_settings))
2731 }
2732
2733 fn ide_controllers(&self) -> Option<&[[Option<Drive>; 2]; 2]> {
2734 match self {
2735 Firmware::Pcat {
2736 ide_controllers, ..
2737 } => Some(ide_controllers),
2738 _ => None,
2739 }
2740 }
2741
2742 fn ide_controllers_mut(&mut self) -> Option<&mut [[Option<Drive>; 2]; 2]> {
2743 match self {
2744 Firmware::Pcat {
2745 ide_controllers, ..
2746 } => Some(ide_controllers),
2747 _ => None,
2748 }
2749 }
2750}
2751
2752#[derive(Debug)]
2755pub enum PcatGuest {
2756 Vhd(BootImageConfig<boot_image_type::Vhd>),
2758 Iso(BootImageConfig<boot_image_type::Iso>),
2760}
2761
2762impl PcatGuest {
2763 fn disk_path(&self) -> DiskPath {
2764 match self {
2765 PcatGuest::Vhd(disk) => disk.disk_path(),
2766 PcatGuest::Iso(disk) => disk.disk_path(),
2767 }
2768 }
2769
2770 fn is_dvd(&self) -> bool {
2771 matches!(self, Self::Iso(_))
2772 }
2773}
2774
2775#[derive(Debug)]
2778pub enum UefiGuest {
2779 Vhd(BootImageConfig<boot_image_type::Vhd>),
2781 GuestTestUefi(ResolvedArtifact),
2783 None,
2785}
2786
2787impl UefiGuest {
2788 pub fn guest_test_uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2790 use petri_artifacts_vmm_test::artifacts::test_vhd::*;
2791 let artifact = match arch {
2792 MachineArch::X86_64 => resolver.require(GUEST_TEST_UEFI_X64).erase(),
2793 MachineArch::Aarch64 => resolver.require(GUEST_TEST_UEFI_AARCH64).erase(),
2794 };
2795 UefiGuest::GuestTestUefi(artifact)
2796 }
2797
2798 fn disk_path(&self) -> Option<DiskPath> {
2799 match self {
2800 UefiGuest::Vhd(vhd) => Some(vhd.disk_path()),
2801 UefiGuest::GuestTestUefi(p) => Some(DiskPath::Local(p.get().to_path_buf())),
2802 UefiGuest::None => None,
2803 }
2804 }
2805}
2806
2807pub mod boot_image_type {
2809 mod private {
2810 pub trait Sealed {}
2811 impl Sealed for super::Vhd {}
2812 impl Sealed for super::Iso {}
2813 }
2814
2815 pub trait BootImageType: private::Sealed {}
2818
2819 #[derive(Debug)]
2821 pub enum Vhd {}
2822
2823 #[derive(Debug)]
2825 pub enum Iso {}
2826
2827 impl BootImageType for Vhd {}
2828 impl BootImageType for Iso {}
2829}
2830
2831#[derive(Debug)]
2833pub struct BootImageConfig<T: boot_image_type::BootImageType> {
2834 artifact: ResolvedArtifactSource,
2836 os_flavor: OsFlavor,
2838 quirks: GuestQuirks,
2842 _type: core::marker::PhantomData<T>,
2844}
2845
2846impl<T: boot_image_type::BootImageType> BootImageConfig<T> {
2847 fn disk_path(&self) -> DiskPath {
2849 match self.artifact.get() {
2850 ArtifactSource::Local(p) => DiskPath::Local(p.clone()),
2851 ArtifactSource::Remote { url } => DiskPath::Remote { url: url.clone() },
2852 }
2853 }
2854}
2855
2856impl BootImageConfig<boot_image_type::Vhd> {
2857 pub fn from_vhd<A>(artifact: ResolvedArtifactSource<A>) -> Self
2859 where
2860 A: petri_artifacts_common::tags::IsTestVhd,
2861 {
2862 BootImageConfig {
2863 artifact: artifact.erase(),
2864 os_flavor: A::OS_FLAVOR,
2865 quirks: A::quirks(),
2866 _type: std::marker::PhantomData,
2867 }
2868 }
2869}
2870
2871impl BootImageConfig<boot_image_type::Iso> {
2872 pub fn from_iso<A>(artifact: ResolvedArtifactSource<A>) -> Self
2874 where
2875 A: petri_artifacts_common::tags::IsTestIso,
2876 {
2877 BootImageConfig {
2878 artifact: artifact.erase(),
2879 os_flavor: A::OS_FLAVOR,
2880 quirks: A::quirks(),
2881 _type: std::marker::PhantomData,
2882 }
2883 }
2884}
2885
2886#[derive(Debug, Clone, Copy)]
2888pub enum IsolationType {
2889 Vbs,
2891 Snp,
2893 Tdx,
2895}
2896
2897#[derive(Debug, Clone, Copy)]
2899pub struct OpenHclServicingFlags {
2900 pub enable_nvme_keepalive: bool,
2903 pub enable_mana_keepalive: bool,
2905 pub override_version_checks: bool,
2907 pub stop_timeout_hint_secs: Option<u16>,
2909}
2910
2911#[derive(Debug, Clone)]
2913pub enum DiskPath {
2914 Local(PathBuf),
2916 Remote {
2918 url: String,
2920 },
2921}
2922
2923impl From<PathBuf> for DiskPath {
2924 fn from(path: PathBuf) -> Self {
2925 DiskPath::Local(path)
2926 }
2927}
2928
2929#[derive(Debug, Clone)]
2931pub enum Disk {
2932 Memory(u64),
2934 Differencing(DiskPath),
2936 Persistent(PathBuf),
2938 Temporary(Arc<TempPath>),
2940}
2941
2942#[derive(Debug, Clone)]
2944pub struct PetriVmgsDisk {
2945 pub disk: Disk,
2947 pub encryption_policy: GuestStateEncryptionPolicy,
2949}
2950
2951impl Default for PetriVmgsDisk {
2952 fn default() -> Self {
2953 PetriVmgsDisk {
2954 disk: Disk::Memory(vmgs_format::VMGS_DEFAULT_CAPACITY),
2955 encryption_policy: GuestStateEncryptionPolicy::None(false),
2957 }
2958 }
2959}
2960
2961#[derive(Debug, Clone)]
2963pub enum PetriVmgsResource {
2964 Disk(PetriVmgsDisk),
2966 ReprovisionOnFailure(PetriVmgsDisk),
2968 Reprovision(PetriVmgsDisk),
2970 Ephemeral,
2972}
2973
2974impl PetriVmgsResource {
2975 pub fn vmgs(&self) -> Option<&PetriVmgsDisk> {
2977 match self {
2978 PetriVmgsResource::Disk(vmgs)
2979 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
2980 | PetriVmgsResource::Reprovision(vmgs) => Some(vmgs),
2981 PetriVmgsResource::Ephemeral => None,
2982 }
2983 }
2984
2985 pub fn disk(&self) -> Option<&Disk> {
2987 self.vmgs().map(|vmgs| &vmgs.disk)
2988 }
2989
2990 pub fn encryption_policy(&self) -> Option<GuestStateEncryptionPolicy> {
2992 self.vmgs().map(|vmgs| vmgs.encryption_policy)
2993 }
2994}
2995
2996#[derive(Debug, Clone, Copy)]
2998pub enum PetriGuestStateLifetime {
2999 Disk,
3002 ReprovisionOnFailure,
3004 Reprovision,
3006 Ephemeral,
3008}
3009
3010#[derive(Debug, Clone, Copy)]
3012pub enum SecureBootTemplate {
3013 MicrosoftWindows,
3015 MicrosoftUefiCertificateAuthority,
3017}
3018
3019#[derive(Default, Debug, Clone)]
3022pub struct VmmQuirks {
3023 pub flaky_boot: Option<Duration>,
3026}
3027
3028fn make_vm_safe_name(name: &str) -> String {
3034 const MAX_VM_NAME_LENGTH: usize = 100;
3035 const HASH_LENGTH: usize = 4;
3036 const MAX_PREFIX_LENGTH: usize = MAX_VM_NAME_LENGTH - HASH_LENGTH;
3037
3038 if name.len() <= MAX_VM_NAME_LENGTH {
3039 name.to_owned()
3040 } else {
3041 let mut hasher = DefaultHasher::new();
3043 name.hash(&mut hasher);
3044 let hash = hasher.finish();
3045
3046 let hash_suffix = format!("{:04x}", hash & 0xFFFF);
3048
3049 let truncated = &name[..MAX_PREFIX_LENGTH];
3051 tracing::debug!(
3052 "VM name too long ({}), truncating '{}' to '{}{}'",
3053 name.len(),
3054 name,
3055 truncated,
3056 hash_suffix
3057 );
3058
3059 format!("{}{}", truncated, hash_suffix)
3060 }
3061}
3062
3063#[derive(Debug, Clone, Copy, Eq, PartialEq)]
3065pub enum PetriHaltReason {
3066 PowerOff,
3068 Reset,
3070 Hibernate,
3072 TripleFault,
3074 Other,
3076}
3077
3078fn append_cmdline(cmd: &mut Option<String>, add_cmd: impl AsRef<str>) {
3079 if let Some(cmd) = cmd.as_mut() {
3080 cmd.push(' ');
3081 cmd.push_str(add_cmd.as_ref());
3082 } else {
3083 *cmd = Some(add_cmd.as_ref().to_string());
3084 }
3085}
3086
3087async fn save_inspect(
3088 name: &str,
3089 inspect: std::pin::Pin<Box<dyn Future<Output = anyhow::Result<inspect::Node>> + Send>>,
3090 log_source: &PetriLogSource,
3091) {
3092 tracing::info!("Collecting {name} inspect details.");
3093 let node = match inspect.await {
3094 Ok(n) => n,
3095 Err(e) => {
3096 tracing::error!(?e, "Failed to get {name}");
3097 return;
3098 }
3099 };
3100 if let Err(e) = log_source.write_attachment(
3101 &format!("timeout_inspect_{name}.log"),
3102 format!("{node:#}").as_bytes(),
3103 ) {
3104 tracing::error!(?e, "Failed to save {name} inspect log");
3105 return;
3106 }
3107 tracing::info!("{name} inspect task finished.");
3108}
3109
3110pub struct ModifyFn<T>(pub Box<dyn FnOnce(T) -> T + Send>);
3112
3113impl<T> Debug for ModifyFn<T> {
3114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3115 write!(f, "_")
3116 }
3117}
3118
3119fn default_vtl2_settings() -> Vtl2Settings {
3121 Vtl2Settings {
3122 version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
3123 fixed: None,
3124 dynamic: Some(Default::default()),
3125 namespace_settings: Default::default(),
3126 }
3127}
3128
3129#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3131pub enum Vtl {
3132 Vtl0 = 0,
3134 Vtl1 = 1,
3136 Vtl2 = 2,
3138}
3139
3140#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3142pub enum VmbusStorageType {
3143 Scsi,
3145 Nvme,
3147 VirtioBlk,
3149}
3150
3151#[derive(Debug, Clone)]
3153pub struct Drive {
3154 pub disk: Option<Disk>,
3156 pub is_dvd: bool,
3158}
3159
3160impl Drive {
3161 pub fn new(disk: Option<Disk>, is_dvd: bool) -> Self {
3163 Self { disk, is_dvd }
3164 }
3165}
3166
3167#[derive(Debug, Clone)]
3169pub struct VmbusStorageController {
3170 pub target_vtl: Vtl,
3172 pub controller_type: VmbusStorageType,
3174 pub drives: HashMap<u32, Drive>,
3176}
3177
3178impl VmbusStorageController {
3179 pub fn new(target_vtl: Vtl, controller_type: VmbusStorageType) -> Self {
3181 Self {
3182 target_vtl,
3183 controller_type,
3184 drives: HashMap::new(),
3185 }
3186 }
3187
3188 pub fn set_drive(
3190 &mut self,
3191 lun: Option<u32>,
3192 drive: Drive,
3193 allow_modify_existing: bool,
3194 ) -> u32 {
3195 let lun = lun.unwrap_or_else(|| {
3196 let mut lun = None;
3198 for x in 0..u8::MAX as u32 {
3199 if !self.drives.contains_key(&x) {
3200 lun = Some(x);
3201 break;
3202 }
3203 }
3204 lun.expect("all locations on this controller are in use")
3205 });
3206
3207 if self.drives.insert(lun, drive).is_some() && !allow_modify_existing {
3208 panic!("a disk with lun {lun} already existed on this controller");
3209 }
3210
3211 lun
3212 }
3213}
3214
3215pub(crate) fn petri_disk_cache_dir() -> String {
3217 if let Ok(dir) = std::env::var("PETRI_CACHE_DIR") {
3218 return dir;
3219 }
3220
3221 #[cfg(target_os = "macos")]
3222 {
3223 if let Ok(home) = std::env::var("HOME") {
3224 return format!("{home}/Library/Caches/petri");
3225 }
3226 }
3227
3228 #[cfg(windows)]
3229 {
3230 if let Ok(local) = std::env::var("LOCALAPPDATA") {
3231 return format!("{local}\\petri\\cache");
3232 }
3233 }
3234
3235 if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
3237 return format!("{xdg}/petri");
3238 }
3239 if let Ok(home) = std::env::var("HOME") {
3240 return format!("{home}/.cache/petri");
3241 }
3242
3243 ".cache/petri".to_string()
3244}
3245
3246#[cfg(test)]
3247mod tests {
3248 use super::make_vm_safe_name;
3249 use crate::Drive;
3250 use crate::VmbusStorageController;
3251 use crate::VmbusStorageType;
3252 use crate::Vtl;
3253
3254 #[test]
3255 fn test_short_names_unchanged() {
3256 let short_name = "short_test_name";
3257 assert_eq!(make_vm_safe_name(short_name), short_name);
3258 }
3259
3260 #[test]
3261 fn test_exactly_100_chars_unchanged() {
3262 let name_100 = "a".repeat(100);
3263 assert_eq!(make_vm_safe_name(&name_100), name_100);
3264 }
3265
3266 #[test]
3267 fn test_long_name_truncated() {
3268 let long_name = "multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_openhcl_servicing";
3269 let result = make_vm_safe_name(long_name);
3270
3271 assert_eq!(result.len(), 100);
3273
3274 assert!(result.starts_with("multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_ope"));
3276
3277 let suffix = &result[96..];
3279 assert_eq!(suffix.len(), 4);
3280 assert!(u16::from_str_radix(suffix, 16).is_ok());
3282 }
3283
3284 #[test]
3285 fn test_deterministic_results() {
3286 let long_name = "very_long_test_name_that_exceeds_the_100_character_limit_and_should_be_truncated_consistently_every_time";
3287 let result1 = make_vm_safe_name(long_name);
3288 let result2 = make_vm_safe_name(long_name);
3289
3290 assert_eq!(result1, result2);
3291 assert_eq!(result1.len(), 100);
3292 }
3293
3294 #[test]
3295 fn test_different_names_different_hashes() {
3296 let name1 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_1";
3297 let name2 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_2";
3298
3299 let result1 = make_vm_safe_name(name1);
3300 let result2 = make_vm_safe_name(name2);
3301
3302 assert_eq!(result1.len(), 100);
3304 assert_eq!(result2.len(), 100);
3305
3306 assert_ne!(result1, result2);
3308 assert_ne!(&result1[96..], &result2[96..]);
3309 }
3310
3311 #[test]
3312 fn test_vmbus_storage_controller() {
3313 let mut controller = VmbusStorageController::new(Vtl::Vtl0, VmbusStorageType::Scsi);
3314 assert_eq!(
3315 controller.set_drive(Some(1), Drive::new(None, false), false),
3316 1
3317 );
3318 assert!(controller.drives.contains_key(&1));
3319 assert_eq!(
3320 controller.set_drive(None, Drive::new(None, false), false),
3321 0
3322 );
3323 assert!(controller.drives.contains_key(&0));
3324 assert_eq!(
3325 controller.set_drive(None, Drive::new(None, false), false),
3326 2
3327 );
3328 assert!(controller.drives.contains_key(&2));
3329 assert_eq!(
3330 controller.set_drive(Some(0), Drive::new(None, false), true),
3331 0
3332 );
3333 assert!(controller.drives.contains_key(&0));
3334 }
3335}