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 mesh::CancelContext;
26use openvmm_defs::config::Vtl2BaseAddressType;
27use pal_async::DefaultDriver;
28use pal_async::task::Spawn;
29use pal_async::task::Task;
30use pal_async::timer::PolledTimer;
31use petri_artifacts_common::tags::GuestQuirks;
32use petri_artifacts_common::tags::GuestQuirksInner;
33use petri_artifacts_common::tags::InitialRebootCondition;
34use petri_artifacts_common::tags::IsOpenhclIgvm;
35use petri_artifacts_common::tags::IsTestVmgs;
36use petri_artifacts_common::tags::MachineArch;
37use petri_artifacts_common::tags::OsFlavor;
38use petri_artifacts_core::ArtifactResolver;
39use petri_artifacts_core::ArtifactSource;
40use petri_artifacts_core::ResolvedArtifact;
41use petri_artifacts_core::ResolvedArtifactSource;
42use petri_artifacts_core::ResolvedOptionalArtifact;
43use pipette_client::PipetteClient;
44use std::collections::BTreeMap;
45use std::collections::HashMap;
46use std::collections::hash_map::DefaultHasher;
47use std::fmt::Debug;
48use std::hash::Hash;
49use std::hash::Hasher;
50use std::path::Path;
51use std::path::PathBuf;
52use std::sync::Arc;
53use std::time::Duration;
54use tempfile::TempPath;
55use vmgs_resources::GuestStateEncryptionPolicy;
56use vtl2_settings_proto::StorageController;
57use vtl2_settings_proto::Vtl2Settings;
58
59pub struct PetriVmArtifacts<T: PetriVmmBackend> {
62 pub backend: T,
64 pub firmware: Firmware,
66 pub arch: MachineArch,
68 pub agent_image: Option<AgentImage>,
70 pub openhcl_agent_image: Option<AgentImage>,
72 pub pipette_binary: Option<ResolvedArtifact>,
74}
75
76impl<T: PetriVmmBackend> PetriVmArtifacts<T> {
77 pub fn new(
81 resolver: &ArtifactResolver<'_>,
82 firmware: Firmware,
83 arch: MachineArch,
84 with_vtl0_pipette: bool,
85 ) -> Option<Self> {
86 if !T::check_compat(&firmware, arch) {
87 return None;
88 }
89
90 let pipette_binary = if with_vtl0_pipette {
91 Some(Self::resolve_pipette_binary(
92 resolver,
93 firmware.os_flavor(),
94 arch,
95 ))
96 } else {
97 None
98 };
99
100 Some(Self {
101 backend: T::new(resolver),
102 arch,
103 agent_image: Some(if with_vtl0_pipette {
104 AgentImage::new(firmware.os_flavor()).with_pipette(resolver, arch)
105 } else {
106 AgentImage::new(firmware.os_flavor())
107 }),
108 openhcl_agent_image: if firmware.is_openhcl() {
109 Some(AgentImage::new(OsFlavor::Linux).with_pipette(resolver, arch))
110 } else {
111 None
112 },
113 pipette_binary,
114 firmware,
115 })
116 }
117
118 fn resolve_pipette_binary(
119 resolver: &ArtifactResolver<'_>,
120 os_flavor: OsFlavor,
121 arch: MachineArch,
122 ) -> ResolvedArtifact {
123 use petri_artifacts_common::artifacts as common_artifacts;
124 match (os_flavor, arch) {
125 (OsFlavor::Linux, MachineArch::X86_64) => resolver
126 .require(common_artifacts::PIPETTE_LINUX_X64)
127 .erase(),
128 (OsFlavor::Linux, MachineArch::Aarch64) => resolver
129 .require(common_artifacts::PIPETTE_LINUX_AARCH64)
130 .erase(),
131 (OsFlavor::Windows, MachineArch::X86_64) => resolver
132 .require(common_artifacts::PIPETTE_WINDOWS_X64)
133 .erase(),
134 (OsFlavor::Windows, MachineArch::Aarch64) => resolver
135 .require(common_artifacts::PIPETTE_WINDOWS_AARCH64)
136 .erase(),
137 (OsFlavor::FreeBsd | OsFlavor::Uefi, _) => {
138 panic!("No pipette binary for this OS flavor")
139 }
140 }
141 }
142}
143
144pub struct PetriVmBuilder<T: PetriVmmBackend> {
146 backend: T,
148 config: PetriVmConfig,
150 modify_vmm_config: Option<ModifyFn<T::VmmConfig>>,
152 resources: PetriVmResources,
154
155 guest_quirks: GuestQuirksInner,
157 vmm_quirks: VmmQuirks,
158
159 expected_boot_event: Option<FirmwareEvent>,
162 override_expect_reset: bool,
163
164 agent_image: Option<AgentImage>,
168 openhcl_agent_image: Option<AgentImage>,
170 boot_device_type: BootDeviceType,
172
173 minimal_mode: bool,
175 pipette_binary: Option<ResolvedArtifact>,
177 enable_serial: bool,
179 enable_screenshots: bool,
181 prebuilt_initrd: Option<PathBuf>,
183 use_virtio_vsock: bool,
185}
186
187impl<T: PetriVmmBackend> Debug for PetriVmBuilder<T> {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 f.debug_struct("PetriVmBuilder")
190 .field("backend", &self.backend)
191 .field("config", &self.config)
192 .field("modify_vmm_config", &self.modify_vmm_config.is_some())
193 .field("resources", &self.resources)
194 .field("guest_quirks", &self.guest_quirks)
195 .field("vmm_quirks", &self.vmm_quirks)
196 .field("expected_boot_event", &self.expected_boot_event)
197 .field("override_expect_reset", &self.override_expect_reset)
198 .field("agent_image", &self.agent_image)
199 .field("openhcl_agent_image", &self.openhcl_agent_image)
200 .field("boot_device_type", &self.boot_device_type)
201 .field("minimal_mode", &self.minimal_mode)
202 .field("enable_serial", &self.enable_serial)
203 .field("enable_screenshots", &self.enable_screenshots)
204 .field("prebuilt_initrd", &self.prebuilt_initrd)
205 .field("use_virtio_vsock", &self.use_virtio_vsock)
206 .finish()
207 }
208}
209
210#[derive(Debug)]
212pub struct PetriVmConfig {
213 pub name: String,
215 pub arch: MachineArch,
217 pub host_log_levels: Option<OpenvmmLogConfig>,
219 pub firmware: Firmware,
221 pub memory: MemoryConfig,
223 pub proc_topology: ProcessorTopology,
225 pub vmgs: PetriVmgsResource,
227 pub tpm: Option<TpmConfig>,
229 pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
231 pub pcie_nvme_drives: Vec<PcieNvmeDrive>,
233}
234
235#[derive(Debug)]
237pub struct PcieNvmeDrive {
238 pub port_name: String,
240 pub nsid: u32,
242 pub drive: Drive,
244}
245
246pub struct PetriVmProperties {
249 pub is_openhcl: bool,
251 pub is_isolated: bool,
253 pub is_pcat: bool,
255 pub is_linux_direct: bool,
257 pub using_vtl0_pipette: bool,
259 pub using_vpci: bool,
261 pub os_flavor: OsFlavor,
263 pub minimal_mode: bool,
265 pub uses_pipette_as_init: bool,
267 pub enable_serial: bool,
269 pub prebuilt_initrd: Option<PathBuf>,
271 pub has_agent_disk: bool,
273 pub use_virtio_vsock: bool,
275}
276
277pub struct PetriVmRuntimeConfig {
279 pub vtl2_settings: Option<Vtl2Settings>,
281 pub ide_controllers: Option<[[Option<Drive>; 2]; 2]>,
283 pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
285}
286
287#[derive(Debug)]
289pub struct PetriVmResources {
290 driver: DefaultDriver,
291 log_source: PetriLogSource,
292}
293
294#[async_trait]
296pub trait PetriVmmBackend: Debug {
297 type VmmConfig;
299
300 type VmRuntime: PetriVmRuntime;
302
303 fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool;
306
307 fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks);
309
310 fn default_servicing_flags() -> OpenHclServicingFlags;
312
313 fn create_guest_dump_disk() -> anyhow::Result<
316 Option<(
317 Arc<TempPath>,
318 Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
319 )>,
320 >;
321
322 fn new(resolver: &ArtifactResolver<'_>) -> Self;
324
325 async fn run(
327 self,
328 config: PetriVmConfig,
329 modify_vmm_config: Option<ModifyFn<Self::VmmConfig>>,
330 resources: &PetriVmResources,
331 properties: PetriVmProperties,
332 ) -> anyhow::Result<(Self::VmRuntime, PetriVmRuntimeConfig)>;
333}
334
335pub(crate) const PETRI_IDE_BOOT_CONTROLLER_NUMBER: u32 = 0;
337pub(crate) const PETRI_IDE_BOOT_LUN: u8 = 0;
338pub(crate) const PETRI_IDE_BOOT_CONTROLLER: Guid =
339 guid::guid!("ca56751f-e643-4bef-bf54-f73678e8b7b5");
340
341pub(crate) const PETRI_SCSI_BOOT_LUN: u32 = 0;
343pub(crate) const PETRI_SCSI_PIPETTE_LUN: u32 = 1;
344pub(crate) const PETRI_SCSI_CRASH_LUN: u32 = 2;
345pub(crate) const PETRI_SCSI_VTL0_CONTROLLER: Guid =
347 guid::guid!("27b553e8-8b39-411b-a55f-839971a7884f");
348pub(crate) const PETRI_SCSI_VTL2_CONTROLLER: Guid =
350 guid::guid!("766e96f8-2ceb-437e-afe3-a93169e48a7c");
351pub(crate) const PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER: Guid =
353 guid::guid!("6c474f47-ed39-49e6-bbb9-142177a1da6e");
354
355pub(crate) const PETRI_NVME_BOOT_NSID: u32 = 37;
357pub(crate) const PETRI_NVME_BOOT_VTL0_CONTROLLER: Guid =
359 guid::guid!("e23a04e2-90f5-4852-bc9d-e7ac691b756c");
360pub(crate) const PETRI_NVME_BOOT_VTL2_CONTROLLER: Guid =
362 guid::guid!("92bc8346-718b-449a-8751-edbf3dcd27e4");
363
364pub struct PetriVm<T: PetriVmmBackend> {
366 resources: PetriVmResources,
367 runtime: T::VmRuntime,
368 watchdog_tasks: Vec<Task<()>>,
369 openhcl_diag_handler: Option<OpenHclDiagHandler>,
370
371 arch: MachineArch,
372 guest_quirks: GuestQuirksInner,
373 vmm_quirks: VmmQuirks,
374 expected_boot_event: Option<FirmwareEvent>,
375 uses_pipette_as_init: bool,
376
377 config: PetriVmRuntimeConfig,
378}
379
380impl<T: PetriVmmBackend> PetriVmBuilder<T> {
381 pub fn new(
383 params: PetriTestParams<'_>,
384 artifacts: PetriVmArtifacts<T>,
385 driver: &DefaultDriver,
386 ) -> anyhow::Result<Self> {
387 let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
388 let expected_boot_event = artifacts.firmware.expected_boot_event();
389 let boot_device_type = match artifacts.firmware {
390 Firmware::LinuxDirect { .. } => BootDeviceType::None,
391 Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
392 Firmware::Pcat { .. } => BootDeviceType::Ide,
393 Firmware::OpenhclPcat { .. } => BootDeviceType::IdeViaScsi,
394 Firmware::Uefi {
395 guest: UefiGuest::None,
396 ..
397 }
398 | Firmware::OpenhclUefi {
399 guest: UefiGuest::None,
400 ..
401 } => BootDeviceType::None,
402 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
403 };
404
405 Ok(Self {
406 backend: artifacts.backend,
407 config: PetriVmConfig {
408 name: make_vm_safe_name(params.test_name),
409 arch: artifacts.arch,
410 host_log_levels: None,
411 firmware: artifacts.firmware,
412 memory: Default::default(),
413 proc_topology: Default::default(),
414
415 vmgs: PetriVmgsResource::Ephemeral,
416 tpm: None,
417 vmbus_storage_controllers: HashMap::new(),
418 pcie_nvme_drives: Vec::new(),
419 },
420 modify_vmm_config: None,
421 resources: PetriVmResources {
422 driver: driver.clone(),
423 log_source: params.logger.clone(),
424 },
425
426 guest_quirks,
427 vmm_quirks,
428 expected_boot_event,
429 override_expect_reset: false,
430
431 agent_image: artifacts.agent_image,
432 openhcl_agent_image: artifacts.openhcl_agent_image,
433 boot_device_type,
434
435 minimal_mode: false,
436 pipette_binary: artifacts.pipette_binary,
437 enable_serial: true,
438 enable_screenshots: true,
439 prebuilt_initrd: None,
440 use_virtio_vsock: false,
441 }
442 .add_petri_scsi_controllers()
443 .add_guest_crash_disk(params.post_test_hooks))
444 }
445
446 pub fn minimal(
457 params: PetriTestParams<'_>,
458 artifacts: PetriVmArtifacts<T>,
459 driver: &DefaultDriver,
460 ) -> anyhow::Result<Self> {
461 let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
462 let expected_boot_event = artifacts.firmware.expected_boot_event();
463 let boot_device_type = match artifacts.firmware {
464 Firmware::LinuxDirect { .. } => BootDeviceType::None,
465 Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
466 Firmware::Pcat { .. } => BootDeviceType::Ide,
467 Firmware::OpenhclPcat { .. } => BootDeviceType::IdeViaScsi,
468 Firmware::Uefi {
469 guest: UefiGuest::None,
470 ..
471 }
472 | Firmware::OpenhclUefi {
473 guest: UefiGuest::None,
474 ..
475 } => BootDeviceType::None,
476 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
477 };
478
479 Ok(Self {
480 backend: artifacts.backend,
481 config: PetriVmConfig {
482 name: make_vm_safe_name(params.test_name),
483 arch: artifacts.arch,
484 host_log_levels: None,
485 firmware: artifacts.firmware,
486 memory: Default::default(),
487 proc_topology: Default::default(),
488
489 vmgs: PetriVmgsResource::Ephemeral,
490 tpm: None,
491 vmbus_storage_controllers: HashMap::new(),
492 pcie_nvme_drives: Vec::new(),
493 },
494 modify_vmm_config: None,
495 resources: PetriVmResources {
496 driver: driver.clone(),
497 log_source: params.logger.clone(),
498 },
499
500 guest_quirks,
501 vmm_quirks,
502 expected_boot_event,
503 override_expect_reset: false,
504
505 agent_image: artifacts.agent_image,
506 openhcl_agent_image: artifacts.openhcl_agent_image,
507 boot_device_type,
508
509 minimal_mode: true,
510 pipette_binary: artifacts.pipette_binary,
511 enable_serial: false,
512 enable_screenshots: true,
513 prebuilt_initrd: None,
514 use_virtio_vsock: false,
515 })
516 }
517
518 pub fn is_minimal(&self) -> bool {
520 self.minimal_mode
521 }
522
523 pub fn with_prebuilt_initrd(mut self, path: PathBuf) -> Self {
530 self.prebuilt_initrd = Some(path);
531 self
532 }
533
534 pub fn prepare_initrd(&self) -> anyhow::Result<TempPath> {
545 use anyhow::Context;
546 use std::io::Write;
547
548 let initrd_path = self
549 .config
550 .firmware
551 .linux_direct_initrd()
552 .context("prepare_initrd requires Linux direct boot with initrd")?;
553 let pipette_path = self
554 .pipette_binary
555 .as_ref()
556 .context("prepare_initrd requires a pipette binary")?;
557
558 let initrd_gz = std::fs::read(initrd_path)
559 .with_context(|| format!("failed to read initrd at {}", initrd_path.display()))?;
560 let pipette_data = std::fs::read(pipette_path.get()).with_context(|| {
561 format!(
562 "failed to read pipette binary at {}",
563 pipette_path.get().display()
564 )
565 })?;
566
567 let merged_gz =
568 crate::cpio::inject_into_initrd(&initrd_gz, "pipette", &pipette_data, 0o100755)
569 .context("failed to inject pipette into initrd")?;
570
571 let mut tmp = tempfile::NamedTempFile::new()
572 .context("failed to create temp file for pre-built initrd")?;
573 tmp.write_all(&merged_gz)
574 .context("failed to write pre-built initrd")?;
575
576 Ok(tmp.into_temp_path())
577 }
578
579 pub fn with_serial_output(mut self) -> Self {
588 self.enable_serial = true;
589 self
590 }
591
592 pub fn without_serial_output(mut self) -> Self {
597 self.enable_serial = false;
598 self
599 }
600
601 pub fn without_screenshots(mut self) -> Self {
606 self.enable_screenshots = false;
607 self
608 }
609
610 pub fn with_virtio_vsock(mut self) -> Self {
621 self.use_virtio_vsock = true;
622 self
623 }
624
625 fn add_petri_scsi_controllers(self) -> Self {
626 let builder = self.add_vmbus_storage_controller(
627 &PETRI_SCSI_VTL0_CONTROLLER,
628 Vtl::Vtl0,
629 VmbusStorageType::Scsi,
630 );
631
632 if builder.is_openhcl() {
633 builder.add_vmbus_storage_controller(
634 &PETRI_SCSI_VTL2_CONTROLLER,
635 Vtl::Vtl2,
636 VmbusStorageType::Scsi,
637 )
638 } else {
639 builder
640 }
641 }
642
643 fn add_guest_crash_disk(self, post_test_hooks: &mut Vec<PetriPostTestHook>) -> Self {
644 let logger = self.resources.log_source.clone();
645 let (disk, disk_hook) = matches!(
646 self.config.firmware.os_flavor(),
647 OsFlavor::Windows | OsFlavor::Linux
648 )
649 .then(|| T::create_guest_dump_disk().expect("failed to create guest dump disk"))
650 .flatten()
651 .unzip();
652
653 if let Some(disk_hook) = disk_hook {
654 post_test_hooks.push(PetriPostTestHook::new(
655 "extract guest crash dumps".into(),
656 move |test_passed| {
657 if test_passed {
658 return Ok(());
659 }
660 let mut disk = disk_hook()?;
661 let gpt = gptman::GPT::read_from(&mut disk, SECTOR_SIZE)?;
662 let partition = fscommon::StreamSlice::new(
663 &mut disk,
664 gpt[1].starting_lba * SECTOR_SIZE,
665 gpt[1].ending_lba * SECTOR_SIZE,
666 )?;
667 let fs = fatfs::FileSystem::new(partition, fatfs::FsOptions::new())?;
668 for entry in fs.root_dir().iter() {
669 let Ok(entry) = entry else {
670 tracing::warn!(?entry, "failed to read entry in guest crash dump disk");
671 continue;
672 };
673 if !entry.is_file() {
674 tracing::warn!(
675 ?entry,
676 "skipping non-file entry in guest crash dump disk"
677 );
678 continue;
679 }
680 logger.write_attachment(&entry.file_name(), entry.to_file())?;
681 }
682 Ok(())
683 },
684 ));
685 }
686
687 if let Some(disk) = disk {
688 self.add_vmbus_drive(
689 Drive::new(Some(Disk::Temporary(disk)), false),
690 &PETRI_SCSI_VTL0_CONTROLLER,
691 Some(PETRI_SCSI_CRASH_LUN),
692 )
693 } else {
694 self
695 }
696 }
697
698 fn add_agent_disks(self) -> Self {
699 self.add_agent_disk_inner(Vtl::Vtl0)
700 .add_agent_disk_inner(Vtl::Vtl2)
701 }
702
703 fn add_agent_disk_inner(mut self, target_vtl: Vtl) -> Self {
704 let (agent_image, controller_id) = match target_vtl {
705 Vtl::Vtl0 => (self.agent_image.as_ref(), PETRI_SCSI_VTL0_CONTROLLER),
706 Vtl::Vtl1 => panic!("no VTL1 agent disk"),
707 Vtl::Vtl2 => (
708 self.openhcl_agent_image.as_ref(),
709 PETRI_SCSI_VTL2_CONTROLLER,
710 ),
711 };
712
713 if target_vtl == Vtl::Vtl0
716 && self.uses_pipette_as_init()
717 && !agent_image.is_some_and(|i| i.has_extras())
718 {
719 return self;
720 }
721
722 let Some(agent_disk) = agent_image.and_then(|i| {
723 i.build(crate::disk_image::ImageType::Vhd)
724 .expect("failed to build agent image")
725 }) else {
726 return self;
727 };
728
729 if !self
732 .config
733 .vmbus_storage_controllers
734 .contains_key(&controller_id)
735 {
736 self = self.add_vmbus_storage_controller(
737 &controller_id,
738 target_vtl,
739 VmbusStorageType::Scsi,
740 );
741 }
742
743 self.add_vmbus_drive(
744 Drive::new(
745 Some(Disk::Temporary(Arc::new(agent_disk.into_temp_path()))),
746 false,
747 ),
748 &controller_id,
749 Some(PETRI_SCSI_PIPETTE_LUN),
750 )
751 }
752
753 fn add_boot_disk(mut self) -> Self {
754 if self.boot_device_type.requires_vtl2() && !self.is_openhcl() {
755 panic!("boot device type {:?} requires vtl2", self.boot_device_type);
756 }
757
758 if self.boot_device_type.requires_vpci_boot() {
759 self.config
760 .firmware
761 .uefi_config_mut()
762 .expect("vpci boot requires uefi")
763 .enable_vpci_boot = true;
764 }
765
766 if let Some(boot_drive) = self.config.firmware.boot_drive() {
767 match self.boot_device_type {
768 BootDeviceType::None => unreachable!(),
769 BootDeviceType::Ide => self.add_ide_drive(
770 boot_drive,
771 PETRI_IDE_BOOT_CONTROLLER_NUMBER,
772 PETRI_IDE_BOOT_LUN,
773 ),
774 BootDeviceType::IdeViaScsi => self
775 .add_vmbus_drive(
776 boot_drive,
777 &PETRI_SCSI_VTL2_CONTROLLER,
778 Some(PETRI_SCSI_BOOT_LUN),
779 )
780 .add_vtl2_storage_controller(
781 Vtl2StorageControllerBuilder::new(ControllerType::Ide)
782 .with_instance_id(PETRI_IDE_BOOT_CONTROLLER)
783 .add_lun(
784 Vtl2LunBuilder::disk()
785 .with_channel(PETRI_IDE_BOOT_CONTROLLER_NUMBER)
786 .with_location(PETRI_IDE_BOOT_LUN as u32)
787 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
788 ControllerType::Scsi,
789 PETRI_SCSI_VTL2_CONTROLLER,
790 PETRI_SCSI_BOOT_LUN,
791 )),
792 )
793 .build(),
794 ),
795 BootDeviceType::IdeViaNvme => todo!(),
796 BootDeviceType::Scsi => self.add_vmbus_drive(
797 boot_drive,
798 &PETRI_SCSI_VTL0_CONTROLLER,
799 Some(PETRI_SCSI_BOOT_LUN),
800 ),
801 BootDeviceType::ScsiViaScsi => self
802 .add_vmbus_drive(
803 boot_drive,
804 &PETRI_SCSI_VTL2_CONTROLLER,
805 Some(PETRI_SCSI_BOOT_LUN),
806 )
807 .add_vtl2_storage_controller(
808 Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
809 .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
810 .add_lun(
811 Vtl2LunBuilder::disk()
812 .with_location(PETRI_SCSI_BOOT_LUN)
813 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
814 ControllerType::Scsi,
815 PETRI_SCSI_VTL2_CONTROLLER,
816 PETRI_SCSI_BOOT_LUN,
817 )),
818 )
819 .build(),
820 ),
821 BootDeviceType::ScsiViaNvme => self
822 .add_vmbus_storage_controller(
823 &PETRI_NVME_BOOT_VTL2_CONTROLLER,
824 Vtl::Vtl2,
825 VmbusStorageType::Nvme,
826 )
827 .add_vmbus_drive(
828 boot_drive,
829 &PETRI_NVME_BOOT_VTL2_CONTROLLER,
830 Some(PETRI_NVME_BOOT_NSID),
831 )
832 .add_vtl2_storage_controller(
833 Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
834 .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
835 .add_lun(
836 Vtl2LunBuilder::disk()
837 .with_location(PETRI_SCSI_BOOT_LUN)
838 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
839 ControllerType::Nvme,
840 PETRI_NVME_BOOT_VTL2_CONTROLLER,
841 PETRI_NVME_BOOT_NSID,
842 )),
843 )
844 .build(),
845 ),
846 BootDeviceType::Nvme => self
847 .add_vmbus_storage_controller(
848 &PETRI_NVME_BOOT_VTL0_CONTROLLER,
849 Vtl::Vtl0,
850 VmbusStorageType::Nvme,
851 )
852 .add_vmbus_drive(
853 boot_drive,
854 &PETRI_NVME_BOOT_VTL0_CONTROLLER,
855 Some(PETRI_NVME_BOOT_NSID),
856 ),
857 BootDeviceType::NvmeViaScsi => todo!(),
858 BootDeviceType::NvmeViaNvme => todo!(),
859 BootDeviceType::PcieNvme => {
860 self.config.pcie_nvme_drives.push(PcieNvmeDrive {
861 port_name: "s0rc0rp0".into(),
862 nsid: 1,
863 drive: boot_drive,
864 });
865 self
866 }
867 }
868 } else {
869 self
870 }
871 }
872
873 fn has_agent_disk(&self) -> bool {
878 if self.uses_pipette_as_init() {
879 self.agent_image.as_ref().is_some_and(|i| i.has_extras())
880 } else {
881 self.agent_image.is_some()
882 }
883 }
884
885 pub fn properties(&self) -> PetriVmProperties {
887 PetriVmProperties {
888 is_openhcl: self.config.firmware.is_openhcl(),
889 is_isolated: self.config.firmware.isolation().is_some(),
890 is_pcat: self.config.firmware.is_pcat(),
891 is_linux_direct: self.config.firmware.is_linux_direct(),
892 using_vtl0_pipette: self.using_vtl0_pipette(),
893 using_vpci: self.boot_device_type.requires_vpci_boot(),
894 os_flavor: self.config.firmware.os_flavor(),
895 minimal_mode: self.minimal_mode,
896 uses_pipette_as_init: self.uses_pipette_as_init(),
897 enable_serial: self.enable_serial,
898 prebuilt_initrd: self.prebuilt_initrd.clone(),
899 has_agent_disk: self.has_agent_disk(),
900 use_virtio_vsock: self.use_virtio_vsock,
901 }
902 }
903
904 fn uses_pipette_as_init(&self) -> bool {
910 self.config.firmware.is_linux_direct()
911 && !self.config.firmware.is_openhcl()
912 && self.pipette_binary.is_some()
913 }
914
915 pub fn using_vtl0_pipette(&self) -> bool {
917 self.uses_pipette_as_init()
918 || self
919 .agent_image
920 .as_ref()
921 .is_some_and(|x| x.contains_pipette())
922 }
923
924 pub async fn run_without_agent(self) -> anyhow::Result<PetriVm<T>> {
928 self.run_core().await
929 }
930
931 pub async fn run(self) -> anyhow::Result<(PetriVm<T>, PipetteClient)> {
934 assert!(self.using_vtl0_pipette());
935
936 let mut vm = self.run_core().await?;
937 let client = vm.wait_for_agent().await?;
938 Ok((vm, client))
939 }
940
941 async fn run_core(mut self) -> anyhow::Result<PetriVm<T>> {
942 self = self.add_boot_disk().add_agent_disks();
945
946 let _prepared_initrd_guard;
950 if self.uses_pipette_as_init() && self.prebuilt_initrd.is_none() {
951 let tmp = self.prepare_initrd()?;
952 self.prebuilt_initrd = Some(tmp.to_path_buf());
953 _prepared_initrd_guard = Some(tmp);
954 } else {
955 _prepared_initrd_guard = None;
956 }
957
958 tracing::debug!(builder = ?self);
959
960 let arch = self.config.arch;
961 let expect_reset = self.expect_reset();
962 let uses_pipette_as_init = self.uses_pipette_as_init();
963 let properties = self.properties();
964
965 let (mut runtime, config) = self
966 .backend
967 .run(
968 self.config,
969 self.modify_vmm_config,
970 &self.resources,
971 properties,
972 )
973 .await?;
974 let openhcl_diag_handler = runtime.openhcl_diag();
975 let watchdog_tasks =
976 Self::start_watchdog_tasks(&self.resources, &mut runtime, self.enable_screenshots)?;
977
978 let mut vm = PetriVm {
979 resources: self.resources,
980 runtime,
981 watchdog_tasks,
982 openhcl_diag_handler,
983
984 arch,
985 guest_quirks: self.guest_quirks,
986 vmm_quirks: self.vmm_quirks,
987 expected_boot_event: self.expected_boot_event,
988 uses_pipette_as_init,
989
990 config,
991 };
992
993 if expect_reset {
994 vm.wait_for_reset_core().await?;
995 }
996
997 vm.wait_for_expected_boot_event().await?;
998
999 Ok(vm)
1000 }
1001
1002 fn expect_reset(&self) -> bool {
1003 self.override_expect_reset
1004 || matches!(
1005 (
1006 self.guest_quirks.initial_reboot,
1007 self.expected_boot_event,
1008 &self.config.firmware,
1009 &self.config.tpm,
1010 ),
1011 (
1012 Some(InitialRebootCondition::Always),
1013 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
1014 _,
1015 _,
1016 ) | (
1017 Some(InitialRebootCondition::WithTpm),
1018 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
1019 _,
1020 Some(_),
1021 )
1022 )
1023 }
1024
1025 fn start_watchdog_tasks(
1026 resources: &PetriVmResources,
1027 runtime: &mut T::VmRuntime,
1028 enable_screenshots: bool,
1029 ) -> anyhow::Result<Vec<Task<()>>> {
1030 let mut tasks = Vec::new();
1031
1032 {
1033 const TIMEOUT_DURATION_MINUTES: u64 = 10;
1034 const TIMER_DURATION: Duration = Duration::from_secs(TIMEOUT_DURATION_MINUTES * 60);
1035 let log_source = resources.log_source.clone();
1036 let inspect_task =
1037 |name,
1038 driver: &DefaultDriver,
1039 inspect: std::pin::Pin<Box<dyn Future<Output = _> + Send>>| {
1040 driver.spawn(format!("petri-watchdog-inspect-{name}"), async move {
1041 if CancelContext::new()
1042 .with_timeout(Duration::from_secs(10))
1043 .until_cancelled(save_inspect(name, inspect, &log_source))
1044 .await
1045 .is_err()
1046 {
1047 tracing::warn!(name, "Failed to collect inspect data within timeout");
1048 }
1049 })
1050 };
1051
1052 let driver = resources.driver.clone();
1053 let vmm_inspector = runtime.inspector();
1054 let openhcl_diag_handler = runtime.openhcl_diag();
1055 tasks.push(resources.driver.spawn("timer-watchdog", async move {
1056 PolledTimer::new(&driver).sleep(TIMER_DURATION).await;
1057 tracing::warn!("Test timeout reached after {TIMEOUT_DURATION_MINUTES} minutes, collecting diagnostics.");
1058 let mut timeout_tasks = Vec::new();
1059 if let Some(inspector) = vmm_inspector {
1060 timeout_tasks.push(inspect_task.clone()("vmm", &driver, Box::pin(async move { inspector.inspect_all().await })) );
1061 }
1062 if let Some(openhcl_diag_handler) = openhcl_diag_handler {
1063 timeout_tasks.push(inspect_task("openhcl", &driver, Box::pin(async move { openhcl_diag_handler.inspect("", None, None).await })));
1064 }
1065 futures::future::join_all(timeout_tasks).await;
1066 tracing::error!("Test time out diagnostics collection complete, aborting.");
1067 panic!("Test timed out");
1068 }));
1069 }
1070
1071 if enable_screenshots {
1072 if let Some(mut framebuffer_access) = runtime.take_framebuffer_access() {
1073 let mut timer = PolledTimer::new(&resources.driver);
1074 let log_source = resources.log_source.clone();
1075
1076 tasks.push(
1077 resources
1078 .driver
1079 .spawn("petri-watchdog-screenshot", async move {
1080 let mut image = Vec::new();
1081 let mut last_image = Vec::new();
1082 loop {
1083 timer.sleep(Duration::from_secs(2)).await;
1084 tracing::trace!("Taking screenshot.");
1085
1086 let VmScreenshotMeta {
1087 color,
1088 width,
1089 height,
1090 } = match framebuffer_access.screenshot(&mut image).await {
1091 Ok(Some(meta)) => meta,
1092 Ok(None) => {
1093 tracing::debug!("VM off, skipping screenshot.");
1094 continue;
1095 }
1096 Err(e) => {
1097 tracing::error!(?e, "Failed to take screenshot");
1098 continue;
1099 }
1100 };
1101
1102 if image == last_image {
1103 tracing::debug!(
1104 "No change in framebuffer, skipping screenshot."
1105 );
1106 continue;
1107 }
1108
1109 let r = log_source.create_attachment("screenshot.png").and_then(
1110 |mut f| {
1111 image::write_buffer_with_format(
1112 &mut f,
1113 &image,
1114 width.into(),
1115 height.into(),
1116 color,
1117 image::ImageFormat::Png,
1118 )
1119 .map_err(Into::into)
1120 },
1121 );
1122
1123 if let Err(e) = r {
1124 tracing::error!(?e, "Failed to save screenshot");
1125 } else {
1126 tracing::info!("Screenshot saved.");
1127 }
1128
1129 std::mem::swap(&mut image, &mut last_image);
1130 }
1131 }),
1132 );
1133 }
1134 }
1135
1136 Ok(tasks)
1137 }
1138
1139 pub fn with_expect_boot_failure(mut self) -> Self {
1142 self.expected_boot_event = Some(FirmwareEvent::BootFailed);
1143 self
1144 }
1145
1146 pub fn with_expect_no_boot_event(mut self) -> Self {
1149 self.expected_boot_event = None;
1150 self
1151 }
1152
1153 pub fn with_expect_reset(mut self) -> Self {
1157 self.override_expect_reset = true;
1158 self
1159 }
1160
1161 pub fn with_secure_boot(mut self) -> Self {
1163 self.config
1164 .firmware
1165 .uefi_config_mut()
1166 .expect("Secure boot is only supported for UEFI firmware.")
1167 .secure_boot_enabled = true;
1168
1169 match self.os_flavor() {
1170 OsFlavor::Windows => self.with_windows_secure_boot_template(),
1171 OsFlavor::Linux => self.with_uefi_ca_secure_boot_template(),
1172 _ => panic!(
1173 "Secure boot unsupported for OS flavor {:?}",
1174 self.os_flavor()
1175 ),
1176 }
1177 }
1178
1179 pub fn with_windows_secure_boot_template(mut self) -> Self {
1181 self.config
1182 .firmware
1183 .uefi_config_mut()
1184 .expect("Secure boot is only supported for UEFI firmware.")
1185 .secure_boot_template = Some(SecureBootTemplate::MicrosoftWindows);
1186 self
1187 }
1188
1189 pub fn with_uefi_ca_secure_boot_template(mut self) -> Self {
1191 self.config
1192 .firmware
1193 .uefi_config_mut()
1194 .expect("Secure boot is only supported for UEFI firmware.")
1195 .secure_boot_template = Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority);
1196 self
1197 }
1198
1199 pub fn with_processor_topology(mut self, topology: ProcessorTopology) -> Self {
1201 self.config.proc_topology = topology;
1202 self
1203 }
1204
1205 pub fn with_memory(mut self, memory: MemoryConfig) -> Self {
1207 self.config.memory = memory;
1208 self
1209 }
1210
1211 pub fn with_vtl2_base_address_type(mut self, address_type: Vtl2BaseAddressType) -> Self {
1216 self.config
1217 .firmware
1218 .openhcl_config_mut()
1219 .expect("OpenHCL firmware is required to set custom VTL2 address type.")
1220 .vtl2_base_address_type = Some(address_type);
1221 self
1222 }
1223
1224 pub fn with_custom_openhcl(mut self, artifact: ResolvedArtifact<impl IsOpenhclIgvm>) -> Self {
1226 match &mut self.config.firmware {
1227 Firmware::OpenhclLinuxDirect { igvm_path, .. }
1228 | Firmware::OpenhclPcat { igvm_path, .. }
1229 | Firmware::OpenhclUefi { igvm_path, .. } => {
1230 *igvm_path = artifact.erase();
1231 }
1232 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } | Firmware::Pcat { .. } => {
1233 panic!("Custom OpenHCL is only supported for OpenHCL firmware.")
1234 }
1235 }
1236 self
1237 }
1238
1239 pub fn with_openhcl_command_line(mut self, additional_command_line: &str) -> Self {
1241 append_cmdline(
1242 &mut self
1243 .config
1244 .firmware
1245 .openhcl_config_mut()
1246 .expect("OpenHCL command line is only supported for OpenHCL firmware.")
1247 .custom_command_line,
1248 additional_command_line,
1249 );
1250 self
1251 }
1252
1253 pub fn with_confidential_filtering(self) -> Self {
1255 if !self.config.firmware.is_openhcl() {
1256 panic!("Confidential filtering is only supported for OpenHCL");
1257 }
1258 self.with_openhcl_command_line(&format!(
1259 "{}=1 {}=0",
1260 underhill_confidentiality::OPENHCL_CONFIDENTIAL_ENV_VAR_NAME,
1261 underhill_confidentiality::OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME
1262 ))
1263 }
1264
1265 pub fn with_openhcl_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1267 self.config
1268 .firmware
1269 .openhcl_config_mut()
1270 .expect("OpenHCL firmware is required to set custom OpenHCL log levels.")
1271 .log_levels = levels;
1272 self
1273 }
1274
1275 pub fn with_host_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1279 if let OpenvmmLogConfig::Custom(ref custom_levels) = levels {
1280 for key in custom_levels.keys() {
1281 if !["OPENVMM_LOG", "OPENVMM_SHOW_SPANS"].contains(&key.as_str()) {
1282 panic!("Unsupported OpenVMM log level key: {}", key);
1283 }
1284 }
1285 }
1286
1287 self.config.host_log_levels = Some(levels.clone());
1288 self
1289 }
1290
1291 pub fn with_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
1293 self.agent_image
1294 .as_mut()
1295 .expect("no guest pipette")
1296 .add_file(name, artifact);
1297 self
1298 }
1299
1300 pub fn with_openhcl_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
1302 self.openhcl_agent_image
1303 .as_mut()
1304 .expect("no openhcl pipette")
1305 .add_file(name, artifact);
1306 self
1307 }
1308
1309 pub fn with_uefi_frontpage(mut self, enable: bool) -> Self {
1311 self.config
1312 .firmware
1313 .uefi_config_mut()
1314 .expect("UEFI frontpage is only supported for UEFI firmware.")
1315 .disable_frontpage = !enable;
1316 self
1317 }
1318
1319 pub fn with_efi_diagnostics_log_level(mut self, level: EfiDiagnosticsLogLevel) -> Self {
1325 self.config
1326 .firmware
1327 .uefi_config_mut()
1328 .expect("EFI diagnostics log level is only supported for UEFI firmware.")
1329 .efi_diagnostics_log_level = level;
1330 self
1331 }
1332
1333 pub fn with_default_boot_always_attempt(mut self, enable: bool) -> Self {
1335 self.config
1336 .firmware
1337 .uefi_config_mut()
1338 .expect("Default boot always attempt is only supported for UEFI firmware.")
1339 .default_boot_always_attempt = enable;
1340 self
1341 }
1342
1343 pub fn with_vmbus_redirect(mut self, enable: bool) -> Self {
1345 self.config
1346 .firmware
1347 .openhcl_config_mut()
1348 .expect("VMBus redirection is only supported for OpenHCL firmware.")
1349 .vmbus_redirect = enable;
1350 self
1351 }
1352
1353 pub fn with_guest_state_lifetime(
1355 mut self,
1356 guest_state_lifetime: PetriGuestStateLifetime,
1357 ) -> Self {
1358 let disk = match self.config.vmgs {
1359 PetriVmgsResource::Disk(disk)
1360 | PetriVmgsResource::ReprovisionOnFailure(disk)
1361 | PetriVmgsResource::Reprovision(disk) => disk,
1362 PetriVmgsResource::Ephemeral => PetriVmgsDisk::default(),
1363 };
1364 self.config.vmgs = match guest_state_lifetime {
1365 PetriGuestStateLifetime::Disk => PetriVmgsResource::Disk(disk),
1366 PetriGuestStateLifetime::ReprovisionOnFailure => {
1367 PetriVmgsResource::ReprovisionOnFailure(disk)
1368 }
1369 PetriGuestStateLifetime::Reprovision => PetriVmgsResource::Reprovision(disk),
1370 PetriGuestStateLifetime::Ephemeral => {
1371 if !matches!(disk.disk, Disk::Memory(_)) {
1372 panic!("attempted to use ephemeral guest state after specifying backing vmgs")
1373 }
1374 PetriVmgsResource::Ephemeral
1375 }
1376 };
1377 self
1378 }
1379
1380 pub fn with_guest_state_encryption(mut self, policy: GuestStateEncryptionPolicy) -> Self {
1382 match &mut self.config.vmgs {
1383 PetriVmgsResource::Disk(vmgs)
1384 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1385 | PetriVmgsResource::Reprovision(vmgs) => {
1386 vmgs.encryption_policy = policy;
1387 }
1388 PetriVmgsResource::Ephemeral => {
1389 panic!("attempted to encrypt ephemeral guest state")
1390 }
1391 }
1392 self
1393 }
1394
1395 pub fn with_initial_vmgs(self, disk: ResolvedArtifact<impl IsTestVmgs>) -> Self {
1397 self.with_backing_vmgs(Disk::Differencing(DiskPath::Local(disk.into())))
1398 }
1399
1400 pub fn with_persistent_vmgs(self, disk: impl AsRef<Path>) -> Self {
1402 self.with_backing_vmgs(Disk::Persistent(disk.as_ref().to_path_buf()))
1403 }
1404
1405 fn with_backing_vmgs(mut self, disk: Disk) -> Self {
1406 match &mut self.config.vmgs {
1407 PetriVmgsResource::Disk(vmgs)
1408 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1409 | PetriVmgsResource::Reprovision(vmgs) => {
1410 if !matches!(vmgs.disk, Disk::Memory(_)) {
1411 panic!("already specified a backing vmgs file");
1412 }
1413 vmgs.disk = disk;
1414 }
1415 PetriVmgsResource::Ephemeral => {
1416 panic!("attempted to specify a backing vmgs with ephemeral guest state")
1417 }
1418 }
1419 self
1420 }
1421
1422 pub fn with_boot_device_type(mut self, boot: BootDeviceType) -> Self {
1426 self.boot_device_type = boot;
1427 self
1428 }
1429
1430 pub fn with_tpm(mut self, enable: bool) -> Self {
1432 if enable {
1433 self.config.tpm.get_or_insert_default();
1434 } else {
1435 self.config.tpm = None;
1436 }
1437 self
1438 }
1439
1440 pub fn with_tpm_state_persistence(mut self, tpm_state_persistence: bool) -> Self {
1442 self.config
1443 .tpm
1444 .as_mut()
1445 .expect("TPM persistence requires a TPM")
1446 .no_persistent_secrets = !tpm_state_persistence;
1447 self
1448 }
1449
1450 pub fn with_custom_vtl2_settings(
1454 mut self,
1455 f: impl FnOnce(&mut Vtl2Settings) + 'static + Send + Sync,
1456 ) -> Self {
1457 f(self
1458 .config
1459 .firmware
1460 .vtl2_settings()
1461 .expect("Custom VTL 2 settings are only supported with OpenHCL"));
1462 self
1463 }
1464
1465 pub fn add_vtl2_storage_controller(self, controller: StorageController) -> Self {
1467 self.with_custom_vtl2_settings(move |v| {
1468 v.dynamic
1469 .as_mut()
1470 .unwrap()
1471 .storage_controllers
1472 .push(controller)
1473 })
1474 }
1475
1476 pub fn add_vmbus_storage_controller(
1478 mut self,
1479 id: &Guid,
1480 target_vtl: Vtl,
1481 controller_type: VmbusStorageType,
1482 ) -> Self {
1483 if self
1484 .config
1485 .vmbus_storage_controllers
1486 .insert(
1487 *id,
1488 VmbusStorageController::new(target_vtl, controller_type),
1489 )
1490 .is_some()
1491 {
1492 panic!("storage controller {id} already existed");
1493 }
1494 self
1495 }
1496
1497 pub fn add_vmbus_drive(
1499 mut self,
1500 drive: Drive,
1501 controller_id: &Guid,
1502 controller_location: Option<u32>,
1503 ) -> Self {
1504 let controller = self
1505 .config
1506 .vmbus_storage_controllers
1507 .get_mut(controller_id)
1508 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
1509
1510 _ = controller.set_drive(controller_location, drive, false);
1511
1512 self
1513 }
1514
1515 pub fn add_ide_drive(
1517 mut self,
1518 drive: Drive,
1519 controller_number: u32,
1520 controller_location: u8,
1521 ) -> Self {
1522 self.config
1523 .firmware
1524 .ide_controllers_mut()
1525 .expect("Host IDE requires PCAT with no HCL")[controller_number as usize]
1526 [controller_location as usize] = Some(drive);
1527
1528 self
1529 }
1530
1531 pub fn os_flavor(&self) -> OsFlavor {
1533 self.config.firmware.os_flavor()
1534 }
1535
1536 pub fn is_openhcl(&self) -> bool {
1538 self.config.firmware.is_openhcl()
1539 }
1540
1541 pub fn isolation(&self) -> Option<IsolationType> {
1543 self.config.firmware.isolation()
1544 }
1545
1546 pub fn arch(&self) -> MachineArch {
1548 self.config.arch
1549 }
1550
1551 pub fn log_source(&self) -> &PetriLogSource {
1553 &self.resources.log_source
1554 }
1555
1556 pub fn default_servicing_flags(&self) -> OpenHclServicingFlags {
1558 T::default_servicing_flags()
1559 }
1560
1561 pub fn modify_backend(
1563 mut self,
1564 f: impl FnOnce(T::VmmConfig) -> T::VmmConfig + 'static + Send,
1565 ) -> Self {
1566 if self.modify_vmm_config.is_some() {
1567 panic!("only one modify_backend allowed");
1568 }
1569 self.modify_vmm_config = Some(ModifyFn(Box::new(f)));
1570 self
1571 }
1572}
1573
1574impl<T: PetriVmmBackend> PetriVm<T> {
1575 pub async fn teardown(self) -> anyhow::Result<()> {
1577 tracing::info!("Tearing down VM...");
1578 self.runtime.teardown().await
1579 }
1580
1581 pub async fn wait_for_halt(&mut self) -> anyhow::Result<PetriHaltReasonDetail> {
1583 tracing::info!("Waiting for VM to halt...");
1584 let halt_reason = self.runtime.wait_for_halt(false).await?;
1585 tracing::info!("VM halted: {halt_reason:?}. Cancelling watchdogs...");
1586 futures::future::join_all(self.watchdog_tasks.drain(..).map(|t| t.cancel())).await;
1587 Ok(halt_reason)
1588 }
1589
1590 pub async fn wait_for_clean_shutdown(&mut self) -> anyhow::Result<()> {
1592 let halt_reason = self.wait_for_halt().await?;
1593 if halt_reason.reason != PetriHaltReason::PowerOff {
1594 anyhow::bail!("Expected PowerOff, got {halt_reason:?}");
1595 }
1596 tracing::info!("VM was cleanly powered off and torn down.");
1597 Ok(())
1598 }
1599
1600 pub async fn wait_for_teardown(mut self) -> anyhow::Result<PetriHaltReasonDetail> {
1603 let halt_reason = self.wait_for_halt().await?;
1604 self.teardown().await?;
1605 Ok(halt_reason)
1606 }
1607
1608 pub async fn wait_for_clean_teardown(mut self) -> anyhow::Result<()> {
1610 self.wait_for_clean_shutdown().await?;
1611 self.teardown().await
1612 }
1613
1614 pub async fn wait_for_reset_no_agent(&mut self) -> anyhow::Result<()> {
1616 self.wait_for_reset_core().await?;
1617 self.wait_for_expected_boot_event().await?;
1618 Ok(())
1619 }
1620
1621 pub async fn wait_for_reset(&mut self) -> anyhow::Result<PipetteClient> {
1623 self.wait_for_reset_no_agent().await?;
1624 self.wait_for_agent().await
1625 }
1626
1627 async fn wait_for_reset_core(&mut self) -> anyhow::Result<()> {
1628 tracing::info!("Waiting for VM to reset...");
1629 let halt_reason = self.runtime.wait_for_halt(true).await?;
1630 if halt_reason.reason != PetriHaltReason::Reset {
1631 anyhow::bail!("Expected reset, got {halt_reason:?}");
1632 }
1633 tracing::info!("VM reset.");
1634 Ok(())
1635 }
1636
1637 pub async fn inspect_openhcl(
1648 &self,
1649 path: impl Into<String>,
1650 depth: Option<usize>,
1651 timeout: Option<Duration>,
1652 ) -> anyhow::Result<inspect::Node> {
1653 self.openhcl_diag()?
1654 .inspect(path.into().as_str(), depth, timeout)
1655 .await
1656 }
1657
1658 pub async fn inspect_update_openhcl(
1668 &self,
1669 path: impl Into<String>,
1670 value: impl Into<String>,
1671 ) -> anyhow::Result<inspect::Value> {
1672 self.openhcl_diag()?
1673 .inspect_update(path.into(), value.into())
1674 .await
1675 }
1676
1677 pub async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()> {
1679 self.inspect_openhcl("", None, None).await.map(|_| ())
1680 }
1681
1682 pub async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()> {
1688 self.openhcl_diag()?.wait_for_vtl2().await
1689 }
1690
1691 pub async fn kmsg(&self) -> anyhow::Result<diag_client::kmsg_stream::KmsgStream> {
1693 self.openhcl_diag()?.kmsg().await
1694 }
1695
1696 pub async fn openhcl_core_dump(&self, name: &str, path: &Path) -> anyhow::Result<()> {
1699 self.openhcl_diag()?.core_dump(name, path).await
1700 }
1701
1702 pub async fn openhcl_crash(&self, name: &str) -> anyhow::Result<()> {
1704 self.openhcl_diag()?.crash(name).await
1705 }
1706
1707 async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient> {
1710 if !self.uses_pipette_as_init {
1720 self.runtime.wait_for_enlightened_shutdown_ready().await?;
1721 }
1722 self.runtime.wait_for_agent(false).await
1723 }
1724
1725 pub async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient> {
1729 self.launch_vtl2_pipette().await?;
1731 self.runtime.wait_for_agent(true).await
1732 }
1733
1734 async fn wait_for_expected_boot_event(&mut self) -> anyhow::Result<()> {
1741 if let Some(expected_event) = self.expected_boot_event {
1742 let event = self.wait_for_boot_event().await?;
1743
1744 anyhow::ensure!(
1745 event == expected_event,
1746 "Did not receive expected boot event"
1747 );
1748 } else {
1749 tracing::warn!("Boot event not emitted for configured firmware or manually ignored.");
1750 }
1751
1752 Ok(())
1753 }
1754
1755 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
1758 tracing::info!("Waiting for boot event...");
1759 let boot_event = loop {
1760 match CancelContext::new()
1761 .with_timeout(self.vmm_quirks.flaky_boot.unwrap_or(Duration::MAX))
1762 .until_cancelled(self.runtime.wait_for_boot_event())
1763 .await
1764 {
1765 Ok(res) => break res?,
1766 Err(_) => {
1767 tracing::error!("Did not get boot event in required time, resetting...");
1768 if let Some(inspector) = self.runtime.inspector() {
1769 save_inspect(
1770 "vmm",
1771 Box::pin(async move { inspector.inspect_all().await }),
1772 &self.resources.log_source,
1773 )
1774 .await;
1775 }
1776
1777 self.runtime.reset().await?;
1778 continue;
1779 }
1780 }
1781 };
1782 tracing::info!("Got boot event: {boot_event:?}");
1783 Ok(boot_event)
1784 }
1785
1786 pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
1789 tracing::info!("Waiting for enlightened shutdown to be ready");
1790 self.runtime.wait_for_enlightened_shutdown_ready().await?;
1791
1792 let mut wait_time = Duration::from_secs(10);
1798
1799 if let Some(duration) = self.guest_quirks.hyperv_shutdown_ic_sleep {
1801 wait_time += duration;
1802 }
1803
1804 tracing::info!(
1805 "Shutdown IC reported ready, waiting for an extra {}s",
1806 wait_time.as_secs()
1807 );
1808 PolledTimer::new(&self.resources.driver)
1809 .sleep(wait_time)
1810 .await;
1811
1812 tracing::info!("Sending enlightened shutdown command");
1813 self.runtime.send_enlightened_shutdown(kind).await
1814 }
1815
1816 pub async fn restart_openhcl(
1819 &mut self,
1820 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1821 flags: OpenHclServicingFlags,
1822 ) -> anyhow::Result<()> {
1823 self.runtime
1824 .restart_openhcl(&new_openhcl.erase(), flags)
1825 .await
1826 }
1827
1828 pub async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()> {
1831 self.runtime.update_command_line(command_line).await
1832 }
1833
1834 pub async fn add_pcie_device(
1836 &mut self,
1837 port_name: String,
1838 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
1839 ) -> anyhow::Result<()> {
1840 self.runtime.add_pcie_device(port_name, resource).await
1841 }
1842
1843 pub async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
1845 self.runtime.remove_pcie_device(port_name).await
1846 }
1847
1848 pub async fn save_openhcl(
1851 &mut self,
1852 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1853 flags: OpenHclServicingFlags,
1854 ) -> anyhow::Result<()> {
1855 self.runtime.save_openhcl(&new_openhcl.erase(), flags).await
1856 }
1857
1858 pub async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
1861 self.runtime.restore_openhcl().await
1862 }
1863
1864 pub fn arch(&self) -> MachineArch {
1866 self.arch
1867 }
1868
1869 pub fn backend(&mut self) -> &mut T::VmRuntime {
1871 &mut self.runtime
1872 }
1873
1874 async fn launch_vtl2_pipette(&self) -> anyhow::Result<()> {
1875 tracing::debug!("Launching VTL 2 pipette...");
1876
1877 let res = self
1879 .openhcl_diag()?
1880 .run_vtl2_command("sh", &["-c", "mkdir /cidata && mount LABEL=cidata /cidata"])
1881 .await?;
1882
1883 if !res.exit_status.success() {
1884 anyhow::bail!("Failed to mount VTL 2 pipette drive: {:?}", res);
1885 }
1886
1887 let res = self
1888 .openhcl_diag()?
1889 .run_detached_vtl2_command("sh", &["-c", "/cidata/pipette 2>&1 | logger &"])
1890 .await?;
1891
1892 if !res.success() {
1893 anyhow::bail!("Failed to spawn VTL 2 pipette: {:?}", res);
1894 }
1895
1896 Ok(())
1897 }
1898
1899 fn openhcl_diag(&self) -> anyhow::Result<&OpenHclDiagHandler> {
1900 if let Some(ohd) = self.openhcl_diag_handler.as_ref() {
1901 Ok(ohd)
1902 } else {
1903 anyhow::bail!("VM is not configured with OpenHCL")
1904 }
1905 }
1906
1907 pub async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
1909 self.runtime.get_guest_state_file().await
1910 }
1911
1912 pub async fn modify_vtl2_settings(
1914 &mut self,
1915 f: impl FnOnce(&mut Vtl2Settings),
1916 ) -> anyhow::Result<()> {
1917 if self.openhcl_diag_handler.is_none() {
1918 panic!("Custom VTL 2 settings are only supported with OpenHCL");
1919 }
1920 f(self
1921 .config
1922 .vtl2_settings
1923 .get_or_insert_with(default_vtl2_settings));
1924 self.runtime
1925 .set_vtl2_settings(self.config.vtl2_settings.as_ref().unwrap())
1926 .await
1927 }
1928
1929 pub fn get_vmbus_storage_controllers(&self) -> &HashMap<Guid, VmbusStorageController> {
1931 &self.config.vmbus_storage_controllers
1932 }
1933
1934 pub async fn set_vmbus_drive(
1936 &mut self,
1937 drive: Drive,
1938 controller_id: &Guid,
1939 controller_location: Option<u32>,
1940 ) -> anyhow::Result<()> {
1941 let controller = self
1942 .config
1943 .vmbus_storage_controllers
1944 .get_mut(controller_id)
1945 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
1946
1947 let controller_location = controller.set_drive(controller_location, drive, true);
1948 let disk = controller.drives.get(&controller_location).unwrap();
1949
1950 self.runtime
1951 .set_vmbus_drive(disk, controller_id, controller_location)
1952 .await?;
1953
1954 Ok(())
1955 }
1956}
1957
1958#[async_trait]
1960pub trait PetriVmRuntime: Send + Sync + 'static {
1961 type VmInspector: PetriVmInspector;
1963 type VmFramebufferAccess: PetriVmFramebufferAccess;
1965
1966 async fn teardown(self) -> anyhow::Result<()>;
1968 async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReasonDetail>;
1971 async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>;
1973 fn openhcl_diag(&self) -> Option<OpenHclDiagHandler>;
1975 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent>;
1978 async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()>;
1981 async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
1983 async fn restart_openhcl(
1986 &mut self,
1987 new_openhcl: &ResolvedArtifact,
1988 flags: OpenHclServicingFlags,
1989 ) -> anyhow::Result<()>;
1990 async fn save_openhcl(
1994 &mut self,
1995 new_openhcl: &ResolvedArtifact,
1996 flags: OpenHclServicingFlags,
1997 ) -> anyhow::Result<()>;
1998 async fn restore_openhcl(&mut self) -> anyhow::Result<()>;
2001 async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()>;
2004 fn inspector(&self) -> Option<Self::VmInspector> {
2006 None
2007 }
2008 fn take_framebuffer_access(&mut self) -> Option<Self::VmFramebufferAccess> {
2011 None
2012 }
2013 async fn reset(&mut self) -> anyhow::Result<()>;
2015 async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2017 Ok(None)
2018 }
2019 async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()>;
2021 async fn set_vmbus_drive(
2023 &mut self,
2024 disk: &Drive,
2025 controller_id: &Guid,
2026 controller_location: u32,
2027 ) -> anyhow::Result<()>;
2028 async fn add_pcie_device(
2030 &mut self,
2031 port_name: String,
2032 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
2033 ) -> anyhow::Result<()> {
2034 let _ = (port_name, resource);
2035 anyhow::bail!("PCIe hotplug not supported by this backend")
2036 }
2037 async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
2039 let _ = port_name;
2040 anyhow::bail!("PCIe hotplug not supported by this backend")
2041 }
2042}
2043
2044#[async_trait]
2046pub trait PetriVmInspector: Send + Sync + 'static {
2047 async fn inspect_all(&self) -> anyhow::Result<inspect::Node>;
2049}
2050
2051pub struct NoPetriVmInspector;
2053#[async_trait]
2054impl PetriVmInspector for NoPetriVmInspector {
2055 async fn inspect_all(&self) -> anyhow::Result<inspect::Node> {
2056 unreachable!()
2057 }
2058}
2059
2060pub struct VmScreenshotMeta {
2062 pub color: image::ExtendedColorType,
2064 pub width: u16,
2066 pub height: u16,
2068}
2069
2070#[async_trait]
2072pub trait PetriVmFramebufferAccess: Send + 'static {
2073 async fn screenshot(&mut self, image: &mut Vec<u8>)
2076 -> anyhow::Result<Option<VmScreenshotMeta>>;
2077}
2078
2079pub struct NoPetriVmFramebufferAccess;
2081#[async_trait]
2082impl PetriVmFramebufferAccess for NoPetriVmFramebufferAccess {
2083 async fn screenshot(
2084 &mut self,
2085 _image: &mut Vec<u8>,
2086 ) -> anyhow::Result<Option<VmScreenshotMeta>> {
2087 unreachable!()
2088 }
2089}
2090
2091#[derive(Debug)]
2093pub struct ProcessorTopology {
2094 pub vp_count: u32,
2096 pub enable_smt: Option<bool>,
2098 pub vps_per_socket: Option<u32>,
2100 pub apic_mode: Option<ApicMode>,
2102}
2103
2104impl Default for ProcessorTopology {
2105 fn default() -> Self {
2106 Self {
2107 vp_count: 2,
2108 enable_smt: None,
2109 vps_per_socket: None,
2110 apic_mode: None,
2111 }
2112 }
2113}
2114
2115impl ProcessorTopology {
2116 pub fn heavy() -> Self {
2118 Self {
2119 vp_count: 16,
2120 vps_per_socket: Some(8),
2121 ..Default::default()
2122 }
2123 }
2124
2125 pub fn very_heavy() -> Self {
2127 Self {
2128 vp_count: 32,
2129 vps_per_socket: Some(16),
2130 ..Default::default()
2131 }
2132 }
2133}
2134
2135#[derive(Debug, Clone, Copy)]
2137pub enum ApicMode {
2138 Xapic,
2140 X2apicSupported,
2142 X2apicEnabled,
2144}
2145
2146#[derive(Debug)]
2148pub struct MemoryConfig {
2149 pub startup_bytes: u64,
2152 pub dynamic_memory_range: Option<(u64, u64)>,
2156 pub numa_mem_sizes: Option<Vec<u64>>,
2159}
2160
2161impl Default for MemoryConfig {
2162 fn default() -> Self {
2163 Self {
2164 startup_bytes: 4 * 1024 * 1024 * 1024, dynamic_memory_range: None,
2166 numa_mem_sizes: None,
2167 }
2168 }
2169}
2170
2171#[derive(Debug)]
2173pub struct UefiConfig {
2174 pub secure_boot_enabled: bool,
2176 pub secure_boot_template: Option<SecureBootTemplate>,
2178 pub disable_frontpage: bool,
2180 pub default_boot_always_attempt: bool,
2182 pub enable_vpci_boot: bool,
2184 pub efi_diagnostics_log_level: EfiDiagnosticsLogLevel,
2186}
2187
2188impl Default for UefiConfig {
2189 fn default() -> Self {
2190 Self {
2191 secure_boot_enabled: false,
2192 secure_boot_template: None,
2193 disable_frontpage: true,
2194 default_boot_always_attempt: false,
2195 enable_vpci_boot: false,
2196 efi_diagnostics_log_level: EfiDiagnosticsLogLevel::Default,
2197 }
2198 }
2199}
2200
2201#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2206pub enum EfiDiagnosticsLogLevel {
2207 #[default]
2209 Default,
2210 Info,
2212 Full,
2214}
2215
2216#[derive(Debug, Clone)]
2218pub enum OpenvmmLogConfig {
2219 TestDefault,
2223 BuiltInDefault,
2226 Custom(BTreeMap<String, String>),
2236}
2237
2238#[derive(Debug)]
2240pub struct OpenHclConfig {
2241 pub vmbus_redirect: bool,
2243 pub custom_command_line: Option<String>,
2247 pub log_levels: OpenvmmLogConfig,
2251 pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
2254 pub vtl2_settings: Option<Vtl2Settings>,
2256}
2257
2258impl OpenHclConfig {
2259 pub fn command_line(&self) -> String {
2262 let mut cmdline = self.custom_command_line.clone();
2263
2264 append_cmdline(&mut cmdline, "OPENHCL_MANA_KEEP_ALIVE=host,privatepool");
2266
2267 match &self.log_levels {
2268 OpenvmmLogConfig::TestDefault => {
2269 let default_log_levels = {
2270 let openhcl_tracing = if let Ok(x) =
2272 std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG"))
2273 {
2274 format!("OPENVMM_LOG={x}")
2275 } else {
2276 "OPENVMM_LOG=debug".to_owned()
2277 };
2278 let openhcl_show_spans = if let Ok(x) = std::env::var("OPENVMM_SHOW_SPANS") {
2279 format!("OPENVMM_SHOW_SPANS={x}")
2280 } else {
2281 "OPENVMM_SHOW_SPANS=true".to_owned()
2282 };
2283 format!("{openhcl_tracing} {openhcl_show_spans}")
2284 };
2285 append_cmdline(&mut cmdline, &default_log_levels);
2286 }
2287 OpenvmmLogConfig::BuiltInDefault => {
2288 }
2290 OpenvmmLogConfig::Custom(levels) => {
2291 levels.iter().for_each(|(key, value)| {
2292 append_cmdline(&mut cmdline, format!("{key}={value}"));
2293 });
2294 }
2295 }
2296
2297 cmdline.unwrap_or_default()
2298 }
2299}
2300
2301impl Default for OpenHclConfig {
2302 fn default() -> Self {
2303 Self {
2304 vmbus_redirect: false,
2305 custom_command_line: None,
2306 log_levels: OpenvmmLogConfig::TestDefault,
2307 vtl2_base_address_type: None,
2308 vtl2_settings: None,
2309 }
2310 }
2311}
2312
2313#[derive(Debug)]
2315pub struct TpmConfig {
2316 pub no_persistent_secrets: bool,
2318}
2319
2320impl Default for TpmConfig {
2321 fn default() -> Self {
2322 Self {
2323 no_persistent_secrets: true,
2324 }
2325 }
2326}
2327
2328#[derive(Debug)]
2332pub enum Firmware {
2333 LinuxDirect {
2335 kernel: ResolvedArtifact,
2337 initrd: ResolvedArtifact,
2339 },
2340 OpenhclLinuxDirect {
2342 igvm_path: ResolvedArtifact,
2344 openhcl_config: OpenHclConfig,
2346 },
2347 Pcat {
2349 guest: PcatGuest,
2351 bios_firmware: ResolvedOptionalArtifact,
2353 svga_firmware: ResolvedOptionalArtifact,
2355 ide_controllers: [[Option<Drive>; 2]; 2],
2357 },
2358 OpenhclPcat {
2360 guest: PcatGuest,
2362 igvm_path: ResolvedArtifact,
2364 bios_firmware: ResolvedOptionalArtifact,
2366 svga_firmware: ResolvedOptionalArtifact,
2368 openhcl_config: OpenHclConfig,
2370 },
2371 Uefi {
2373 guest: UefiGuest,
2375 uefi_firmware: ResolvedArtifact,
2377 uefi_config: UefiConfig,
2379 },
2380 OpenhclUefi {
2382 guest: UefiGuest,
2384 isolation: Option<IsolationType>,
2386 igvm_path: ResolvedArtifact,
2388 uefi_config: UefiConfig,
2390 openhcl_config: OpenHclConfig,
2392 },
2393}
2394
2395#[derive(Debug, Copy, Clone, PartialEq, Eq)]
2397pub enum BootDeviceType {
2398 None,
2400 Ide,
2402 IdeViaScsi,
2404 IdeViaNvme,
2406 Scsi,
2408 ScsiViaScsi,
2410 ScsiViaNvme,
2412 Nvme,
2414 NvmeViaScsi,
2416 NvmeViaNvme,
2418 PcieNvme,
2420}
2421
2422impl BootDeviceType {
2423 fn requires_vtl2(&self) -> bool {
2424 match self {
2425 BootDeviceType::None
2426 | BootDeviceType::Ide
2427 | BootDeviceType::Scsi
2428 | BootDeviceType::Nvme
2429 | BootDeviceType::PcieNvme => false,
2430 BootDeviceType::IdeViaScsi
2431 | BootDeviceType::IdeViaNvme
2432 | BootDeviceType::ScsiViaScsi
2433 | BootDeviceType::ScsiViaNvme
2434 | BootDeviceType::NvmeViaScsi
2435 | BootDeviceType::NvmeViaNvme => true,
2436 }
2437 }
2438
2439 fn requires_vpci_boot(&self) -> bool {
2440 matches!(
2441 self,
2442 BootDeviceType::Nvme | BootDeviceType::NvmeViaScsi | BootDeviceType::NvmeViaNvme
2443 )
2444 }
2445}
2446
2447impl Firmware {
2448 pub fn linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2450 use petri_artifacts_vmm_test::artifacts::loadable::*;
2451 match arch {
2452 MachineArch::X86_64 => Firmware::LinuxDirect {
2453 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_X64).erase(),
2454 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
2455 },
2456 MachineArch::Aarch64 => Firmware::LinuxDirect {
2457 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_AARCH64).erase(),
2458 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_AARCH64).erase(),
2459 },
2460 }
2461 }
2462
2463 pub fn linux_direct_bzimage(resolver: &ArtifactResolver<'_>) -> Self {
2468 use petri_artifacts_vmm_test::artifacts::loadable::*;
2469 Firmware::LinuxDirect {
2470 kernel: resolver.require(LINUX_DIRECT_TEST_BZIMAGE_X64).erase(),
2471 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
2472 }
2473 }
2474
2475 pub fn openhcl_linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2477 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2478 match arch {
2479 MachineArch::X86_64 => Firmware::OpenhclLinuxDirect {
2480 igvm_path: resolver.require(LATEST_LINUX_DIRECT_TEST_X64).erase(),
2481 openhcl_config: Default::default(),
2482 },
2483 MachineArch::Aarch64 => todo!("Linux direct not yet supported on aarch64"),
2484 }
2485 }
2486
2487 pub fn pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2489 use petri_artifacts_vmm_test::artifacts::loadable::*;
2490 Firmware::Pcat {
2491 guest,
2492 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2493 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2494 ide_controllers: [[None, None], [None, None]],
2495 }
2496 }
2497
2498 pub fn openhcl_pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2500 use petri_artifacts_vmm_test::artifacts::loadable::*;
2501 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2502 Firmware::OpenhclPcat {
2503 guest,
2504 igvm_path: resolver.require(LATEST_STANDARD_X64).erase(),
2505 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2506 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2507 openhcl_config: OpenHclConfig {
2508 vmbus_redirect: true,
2510 ..Default::default()
2511 },
2512 }
2513 }
2514
2515 pub fn uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch, guest: UefiGuest) -> Self {
2517 use petri_artifacts_vmm_test::artifacts::loadable::*;
2518 let uefi_firmware = match arch {
2519 MachineArch::X86_64 => resolver.require(UEFI_FIRMWARE_X64).erase(),
2520 MachineArch::Aarch64 => resolver.require(UEFI_FIRMWARE_AARCH64).erase(),
2521 };
2522 Firmware::Uefi {
2523 guest,
2524 uefi_firmware,
2525 uefi_config: Default::default(),
2526 }
2527 }
2528
2529 pub fn openhcl_uefi(
2531 resolver: &ArtifactResolver<'_>,
2532 arch: MachineArch,
2533 guest: UefiGuest,
2534 isolation: Option<IsolationType>,
2535 ) -> Self {
2536 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2537 let igvm_path = match arch {
2538 MachineArch::X86_64 if isolation.is_some() => resolver.require(LATEST_CVM_X64).erase(),
2539 MachineArch::X86_64 => resolver.require(LATEST_STANDARD_X64).erase(),
2540 MachineArch::Aarch64 => resolver.require(LATEST_STANDARD_AARCH64).erase(),
2541 };
2542 Firmware::OpenhclUefi {
2543 guest,
2544 isolation,
2545 igvm_path,
2546 uefi_config: Default::default(),
2547 openhcl_config: Default::default(),
2548 }
2549 }
2550
2551 fn is_openhcl(&self) -> bool {
2552 match self {
2553 Firmware::OpenhclLinuxDirect { .. }
2554 | Firmware::OpenhclUefi { .. }
2555 | Firmware::OpenhclPcat { .. } => true,
2556 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => false,
2557 }
2558 }
2559
2560 fn isolation(&self) -> Option<IsolationType> {
2561 match self {
2562 Firmware::OpenhclUefi { isolation, .. } => *isolation,
2563 Firmware::LinuxDirect { .. }
2564 | Firmware::Pcat { .. }
2565 | Firmware::Uefi { .. }
2566 | Firmware::OpenhclLinuxDirect { .. }
2567 | Firmware::OpenhclPcat { .. } => None,
2568 }
2569 }
2570
2571 fn is_linux_direct(&self) -> bool {
2572 match self {
2573 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => true,
2574 Firmware::Pcat { .. }
2575 | Firmware::Uefi { .. }
2576 | Firmware::OpenhclUefi { .. }
2577 | Firmware::OpenhclPcat { .. } => false,
2578 }
2579 }
2580
2581 pub fn linux_direct_initrd(&self) -> Option<&Path> {
2583 match self {
2584 Firmware::LinuxDirect { initrd, .. } => Some(initrd.get()),
2585 _ => None,
2586 }
2587 }
2588
2589 fn is_pcat(&self) -> bool {
2590 match self {
2591 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => true,
2592 Firmware::Uefi { .. }
2593 | Firmware::OpenhclUefi { .. }
2594 | Firmware::LinuxDirect { .. }
2595 | Firmware::OpenhclLinuxDirect { .. } => false,
2596 }
2597 }
2598
2599 fn os_flavor(&self) -> OsFlavor {
2600 match self {
2601 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => OsFlavor::Linux,
2602 Firmware::Uefi {
2603 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2604 ..
2605 }
2606 | Firmware::OpenhclUefi {
2607 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2608 ..
2609 } => OsFlavor::Uefi,
2610 Firmware::Pcat {
2611 guest: PcatGuest::Vhd(cfg),
2612 ..
2613 }
2614 | Firmware::OpenhclPcat {
2615 guest: PcatGuest::Vhd(cfg),
2616 ..
2617 }
2618 | Firmware::Uefi {
2619 guest: UefiGuest::Vhd(cfg),
2620 ..
2621 }
2622 | Firmware::OpenhclUefi {
2623 guest: UefiGuest::Vhd(cfg),
2624 ..
2625 } => cfg.os_flavor,
2626 Firmware::Pcat {
2627 guest: PcatGuest::Iso(cfg),
2628 ..
2629 }
2630 | Firmware::OpenhclPcat {
2631 guest: PcatGuest::Iso(cfg),
2632 ..
2633 } => cfg.os_flavor,
2634 }
2635 }
2636
2637 fn quirks(&self) -> GuestQuirks {
2638 match self {
2639 Firmware::Pcat {
2640 guest: PcatGuest::Vhd(cfg),
2641 ..
2642 }
2643 | Firmware::Uefi {
2644 guest: UefiGuest::Vhd(cfg),
2645 ..
2646 }
2647 | Firmware::OpenhclUefi {
2648 guest: UefiGuest::Vhd(cfg),
2649 ..
2650 } => cfg.quirks.clone(),
2651 Firmware::Pcat {
2652 guest: PcatGuest::Iso(cfg),
2653 ..
2654 } => cfg.quirks.clone(),
2655 _ => Default::default(),
2656 }
2657 }
2658
2659 fn expected_boot_event(&self) -> Option<FirmwareEvent> {
2660 match self {
2661 Firmware::LinuxDirect { .. }
2662 | Firmware::OpenhclLinuxDirect { .. }
2663 | Firmware::Uefi {
2664 guest: UefiGuest::GuestTestUefi(_),
2665 ..
2666 }
2667 | Firmware::OpenhclUefi {
2668 guest: UefiGuest::GuestTestUefi(_),
2669 ..
2670 } => None,
2671 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => {
2672 Some(FirmwareEvent::BootAttempt)
2674 }
2675 Firmware::Uefi {
2676 guest: UefiGuest::None,
2677 ..
2678 }
2679 | Firmware::OpenhclUefi {
2680 guest: UefiGuest::None,
2681 ..
2682 } => Some(FirmwareEvent::NoBootDevice),
2683 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => {
2684 Some(FirmwareEvent::BootSuccess)
2685 }
2686 }
2687 }
2688
2689 fn openhcl_config(&self) -> Option<&OpenHclConfig> {
2690 match self {
2691 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2692 | Firmware::OpenhclUefi { openhcl_config, .. }
2693 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
2694 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2695 }
2696 }
2697
2698 fn openhcl_config_mut(&mut self) -> Option<&mut OpenHclConfig> {
2699 match self {
2700 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2701 | Firmware::OpenhclUefi { openhcl_config, .. }
2702 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
2703 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2704 }
2705 }
2706
2707 #[cfg_attr(not(windows), expect(dead_code))]
2708 fn openhcl_firmware(&self) -> Option<&Path> {
2709 match self {
2710 Firmware::OpenhclLinuxDirect { igvm_path, .. }
2711 | Firmware::OpenhclUefi { igvm_path, .. }
2712 | Firmware::OpenhclPcat { igvm_path, .. } => Some(igvm_path.get()),
2713 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2714 }
2715 }
2716
2717 fn into_runtime_config(
2718 self,
2719 vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
2720 ) -> PetriVmRuntimeConfig {
2721 match self {
2722 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2723 | Firmware::OpenhclUefi { openhcl_config, .. }
2724 | Firmware::OpenhclPcat { openhcl_config, .. } => PetriVmRuntimeConfig {
2725 vtl2_settings: Some(
2726 openhcl_config
2727 .vtl2_settings
2728 .unwrap_or_else(default_vtl2_settings),
2729 ),
2730 ide_controllers: None,
2731 vmbus_storage_controllers,
2732 },
2733 Firmware::Pcat {
2734 ide_controllers, ..
2735 } => PetriVmRuntimeConfig {
2736 vtl2_settings: None,
2737 ide_controllers: Some(ide_controllers),
2738 vmbus_storage_controllers,
2739 },
2740 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } => PetriVmRuntimeConfig {
2741 vtl2_settings: None,
2742 ide_controllers: None,
2743 vmbus_storage_controllers,
2744 },
2745 }
2746 }
2747
2748 fn uefi_config(&self) -> Option<&UefiConfig> {
2749 match self {
2750 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
2751 Some(uefi_config)
2752 }
2753 Firmware::LinuxDirect { .. }
2754 | Firmware::OpenhclLinuxDirect { .. }
2755 | Firmware::Pcat { .. }
2756 | Firmware::OpenhclPcat { .. } => None,
2757 }
2758 }
2759
2760 fn uefi_config_mut(&mut self) -> Option<&mut UefiConfig> {
2761 match self {
2762 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
2763 Some(uefi_config)
2764 }
2765 Firmware::LinuxDirect { .. }
2766 | Firmware::OpenhclLinuxDirect { .. }
2767 | Firmware::Pcat { .. }
2768 | Firmware::OpenhclPcat { .. } => None,
2769 }
2770 }
2771
2772 fn boot_drive(&self) -> Option<Drive> {
2773 match self {
2774 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => None,
2775 Firmware::Pcat { guest, .. } | Firmware::OpenhclPcat { guest, .. } => {
2776 Some((guest.disk_path(), guest.is_dvd()))
2777 }
2778 Firmware::Uefi { guest, .. } | Firmware::OpenhclUefi { guest, .. } => {
2779 guest.disk_path().map(|dp| (dp, false))
2780 }
2781 }
2782 .map(|(disk_path, is_dvd)| Drive::new(Some(Disk::Differencing(disk_path)), is_dvd))
2783 }
2784
2785 fn vtl2_settings(&mut self) -> Option<&mut Vtl2Settings> {
2786 self.openhcl_config_mut()
2787 .map(|c| c.vtl2_settings.get_or_insert_with(default_vtl2_settings))
2788 }
2789
2790 fn ide_controllers(&self) -> Option<&[[Option<Drive>; 2]; 2]> {
2791 match self {
2792 Firmware::Pcat {
2793 ide_controllers, ..
2794 } => Some(ide_controllers),
2795 _ => None,
2796 }
2797 }
2798
2799 fn ide_controllers_mut(&mut self) -> Option<&mut [[Option<Drive>; 2]; 2]> {
2800 match self {
2801 Firmware::Pcat {
2802 ide_controllers, ..
2803 } => Some(ide_controllers),
2804 _ => None,
2805 }
2806 }
2807}
2808
2809#[derive(Debug)]
2812pub enum PcatGuest {
2813 Vhd(BootImageConfig<boot_image_type::Vhd>),
2815 Iso(BootImageConfig<boot_image_type::Iso>),
2817}
2818
2819impl PcatGuest {
2820 fn disk_path(&self) -> DiskPath {
2821 match self {
2822 PcatGuest::Vhd(disk) => disk.disk_path(),
2823 PcatGuest::Iso(disk) => disk.disk_path(),
2824 }
2825 }
2826
2827 fn is_dvd(&self) -> bool {
2828 matches!(self, Self::Iso(_))
2829 }
2830}
2831
2832#[derive(Debug)]
2835pub enum UefiGuest {
2836 Vhd(BootImageConfig<boot_image_type::Vhd>),
2838 GuestTestUefi(ResolvedArtifact),
2840 None,
2842}
2843
2844impl UefiGuest {
2845 pub fn guest_test_uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2847 use petri_artifacts_vmm_test::artifacts::test_vhd::*;
2848 let artifact = match arch {
2849 MachineArch::X86_64 => resolver.require(GUEST_TEST_UEFI_X64).erase(),
2850 MachineArch::Aarch64 => resolver.require(GUEST_TEST_UEFI_AARCH64).erase(),
2851 };
2852 UefiGuest::GuestTestUefi(artifact)
2853 }
2854
2855 fn disk_path(&self) -> Option<DiskPath> {
2856 match self {
2857 UefiGuest::Vhd(vhd) => Some(vhd.disk_path()),
2858 UefiGuest::GuestTestUefi(p) => Some(DiskPath::Local(p.get().to_path_buf())),
2859 UefiGuest::None => None,
2860 }
2861 }
2862}
2863
2864pub mod boot_image_type {
2866 mod private {
2867 pub trait Sealed {}
2868 impl Sealed for super::Vhd {}
2869 impl Sealed for super::Iso {}
2870 }
2871
2872 pub trait BootImageType: private::Sealed {}
2875
2876 #[derive(Debug)]
2878 pub enum Vhd {}
2879
2880 #[derive(Debug)]
2882 pub enum Iso {}
2883
2884 impl BootImageType for Vhd {}
2885 impl BootImageType for Iso {}
2886}
2887
2888#[derive(Debug)]
2890pub struct BootImageConfig<T: boot_image_type::BootImageType> {
2891 artifact: ResolvedArtifactSource,
2893 os_flavor: OsFlavor,
2895 quirks: GuestQuirks,
2899 _type: core::marker::PhantomData<T>,
2901}
2902
2903impl<T: boot_image_type::BootImageType> BootImageConfig<T> {
2904 fn disk_path(&self) -> DiskPath {
2906 match self.artifact.get() {
2907 ArtifactSource::Local(p) => DiskPath::Local(p.clone()),
2908 ArtifactSource::Remote { url } => DiskPath::Remote { url: url.clone() },
2909 }
2910 }
2911}
2912
2913impl BootImageConfig<boot_image_type::Vhd> {
2914 pub fn from_vhd<A>(artifact: ResolvedArtifactSource<A>) -> Self
2916 where
2917 A: petri_artifacts_common::tags::IsTestVhd,
2918 {
2919 BootImageConfig {
2920 artifact: artifact.erase(),
2921 os_flavor: A::OS_FLAVOR,
2922 quirks: A::quirks(),
2923 _type: std::marker::PhantomData,
2924 }
2925 }
2926}
2927
2928impl BootImageConfig<boot_image_type::Iso> {
2929 pub fn from_iso<A>(artifact: ResolvedArtifactSource<A>) -> Self
2931 where
2932 A: petri_artifacts_common::tags::IsTestIso,
2933 {
2934 BootImageConfig {
2935 artifact: artifact.erase(),
2936 os_flavor: A::OS_FLAVOR,
2937 quirks: A::quirks(),
2938 _type: std::marker::PhantomData,
2939 }
2940 }
2941}
2942
2943#[derive(Debug, Clone, Copy)]
2945pub enum IsolationType {
2946 Vbs,
2948 Snp,
2950 Tdx,
2952}
2953
2954#[derive(Debug, Clone, Copy)]
2956pub struct OpenHclServicingFlags {
2957 pub enable_nvme_keepalive: bool,
2960 pub enable_mana_keepalive: bool,
2962 pub override_version_checks: bool,
2964 pub stop_timeout_hint_secs: Option<u16>,
2966}
2967
2968#[derive(Debug, Clone)]
2970pub enum DiskPath {
2971 Local(PathBuf),
2973 Remote {
2975 url: String,
2977 },
2978}
2979
2980impl From<PathBuf> for DiskPath {
2981 fn from(path: PathBuf) -> Self {
2982 DiskPath::Local(path)
2983 }
2984}
2985
2986#[derive(Debug, Clone)]
2988pub enum Disk {
2989 Memory(u64),
2991 Differencing(DiskPath),
2993 Persistent(PathBuf),
2995 Temporary(Arc<TempPath>),
2997}
2998
2999#[derive(Debug, Clone)]
3001pub struct PetriVmgsDisk {
3002 pub disk: Disk,
3004 pub encryption_policy: GuestStateEncryptionPolicy,
3006}
3007
3008impl Default for PetriVmgsDisk {
3009 fn default() -> Self {
3010 PetriVmgsDisk {
3011 disk: Disk::Memory(vmgs_format::VMGS_DEFAULT_CAPACITY),
3012 encryption_policy: GuestStateEncryptionPolicy::None(false),
3014 }
3015 }
3016}
3017
3018#[derive(Debug, Clone)]
3020pub enum PetriVmgsResource {
3021 Disk(PetriVmgsDisk),
3023 ReprovisionOnFailure(PetriVmgsDisk),
3025 Reprovision(PetriVmgsDisk),
3027 Ephemeral,
3029}
3030
3031impl PetriVmgsResource {
3032 pub fn vmgs(&self) -> Option<&PetriVmgsDisk> {
3034 match self {
3035 PetriVmgsResource::Disk(vmgs)
3036 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
3037 | PetriVmgsResource::Reprovision(vmgs) => Some(vmgs),
3038 PetriVmgsResource::Ephemeral => None,
3039 }
3040 }
3041
3042 pub fn disk(&self) -> Option<&Disk> {
3044 self.vmgs().map(|vmgs| &vmgs.disk)
3045 }
3046
3047 pub fn encryption_policy(&self) -> Option<GuestStateEncryptionPolicy> {
3049 self.vmgs().map(|vmgs| vmgs.encryption_policy)
3050 }
3051}
3052
3053#[derive(Debug, Clone, Copy)]
3055pub enum PetriGuestStateLifetime {
3056 Disk,
3059 ReprovisionOnFailure,
3061 Reprovision,
3063 Ephemeral,
3065}
3066
3067#[derive(Debug, Clone, Copy)]
3069pub enum SecureBootTemplate {
3070 MicrosoftWindows,
3072 MicrosoftUefiCertificateAuthority,
3074}
3075
3076#[derive(Default, Debug, Clone)]
3079pub struct VmmQuirks {
3080 pub flaky_boot: Option<Duration>,
3083}
3084
3085fn make_vm_safe_name(name: &str) -> String {
3091 const MAX_VM_NAME_LENGTH: usize = 100;
3092 const HASH_LENGTH: usize = 4;
3093 const MAX_PREFIX_LENGTH: usize = MAX_VM_NAME_LENGTH - HASH_LENGTH;
3094
3095 if name.len() <= MAX_VM_NAME_LENGTH {
3096 name.to_owned()
3097 } else {
3098 let mut hasher = DefaultHasher::new();
3100 name.hash(&mut hasher);
3101 let hash = hasher.finish();
3102
3103 let hash_suffix = format!("{:04x}", hash & 0xFFFF);
3105
3106 let truncated = &name[..MAX_PREFIX_LENGTH];
3108 tracing::debug!(
3109 "VM name too long ({}), truncating '{}' to '{}{}'",
3110 name.len(),
3111 name,
3112 truncated,
3113 hash_suffix
3114 );
3115
3116 format!("{}{}", truncated, hash_suffix)
3117 }
3118}
3119
3120#[derive(Debug, Clone, Copy, Eq, PartialEq)]
3122pub enum PetriHaltReason {
3123 PowerOff,
3125 Reset,
3127 Hibernate,
3129 TripleFault,
3131 Other,
3133}
3134
3135impl PetriHaltReason {
3136 pub fn with_detail(self, detail: String) -> PetriHaltReasonDetail {
3138 PetriHaltReasonDetail {
3139 reason: self,
3140 detail,
3141 }
3142 }
3143}
3144
3145#[derive(Debug, Clone)]
3147pub struct PetriHaltReasonDetail {
3148 pub reason: PetriHaltReason,
3150 pub detail: String,
3152}
3153
3154fn append_cmdline(cmd: &mut Option<String>, add_cmd: impl AsRef<str>) {
3155 if let Some(cmd) = cmd.as_mut() {
3156 cmd.push(' ');
3157 cmd.push_str(add_cmd.as_ref());
3158 } else {
3159 *cmd = Some(add_cmd.as_ref().to_string());
3160 }
3161}
3162
3163async fn save_inspect(
3164 name: &str,
3165 inspect: std::pin::Pin<Box<dyn Future<Output = anyhow::Result<inspect::Node>> + Send>>,
3166 log_source: &PetriLogSource,
3167) {
3168 tracing::info!("Collecting {name} inspect details.");
3169 let node = match inspect.await {
3170 Ok(n) => n,
3171 Err(e) => {
3172 tracing::error!(?e, "Failed to get {name}");
3173 return;
3174 }
3175 };
3176 if let Err(e) = log_source.write_attachment(
3177 &format!("timeout_inspect_{name}.log"),
3178 format!("{node:#}").as_bytes(),
3179 ) {
3180 tracing::error!(?e, "Failed to save {name} inspect log");
3181 return;
3182 }
3183 tracing::info!("{name} inspect task finished.");
3184}
3185
3186pub struct ModifyFn<T>(pub Box<dyn FnOnce(T) -> T + Send>);
3188
3189impl<T> Debug for ModifyFn<T> {
3190 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3191 write!(f, "_")
3192 }
3193}
3194
3195fn default_vtl2_settings() -> Vtl2Settings {
3197 Vtl2Settings {
3198 version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
3199 fixed: None,
3200 dynamic: Some(Default::default()),
3201 namespace_settings: Default::default(),
3202 }
3203}
3204
3205#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3207pub enum Vtl {
3208 Vtl0 = 0,
3210 Vtl1 = 1,
3212 Vtl2 = 2,
3214}
3215
3216#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3218pub enum VmbusStorageType {
3219 Scsi,
3221 Nvme,
3223 VirtioBlk,
3225}
3226
3227#[derive(Debug, Clone)]
3229pub struct Drive {
3230 pub disk: Option<Disk>,
3232 pub is_dvd: bool,
3234}
3235
3236impl Drive {
3237 pub fn new(disk: Option<Disk>, is_dvd: bool) -> Self {
3239 Self { disk, is_dvd }
3240 }
3241}
3242
3243#[derive(Debug, Clone)]
3245pub struct VmbusStorageController {
3246 pub target_vtl: Vtl,
3248 pub controller_type: VmbusStorageType,
3250 pub drives: HashMap<u32, Drive>,
3252}
3253
3254impl VmbusStorageController {
3255 pub fn new(target_vtl: Vtl, controller_type: VmbusStorageType) -> Self {
3257 Self {
3258 target_vtl,
3259 controller_type,
3260 drives: HashMap::new(),
3261 }
3262 }
3263
3264 pub fn set_drive(
3266 &mut self,
3267 lun: Option<u32>,
3268 drive: Drive,
3269 allow_modify_existing: bool,
3270 ) -> u32 {
3271 let lun = lun.unwrap_or_else(|| {
3272 let mut lun = None;
3274 for x in 0..u8::MAX as u32 {
3275 if !self.drives.contains_key(&x) {
3276 lun = Some(x);
3277 break;
3278 }
3279 }
3280 lun.expect("all locations on this controller are in use")
3281 });
3282
3283 if self.drives.insert(lun, drive).is_some() && !allow_modify_existing {
3284 panic!("a disk with lun {lun} already existed on this controller");
3285 }
3286
3287 lun
3288 }
3289}
3290
3291pub(crate) fn petri_disk_cache_dir() -> String {
3293 if let Ok(dir) = std::env::var("PETRI_CACHE_DIR") {
3294 return dir;
3295 }
3296
3297 #[cfg(target_os = "macos")]
3298 {
3299 if let Ok(home) = std::env::var("HOME") {
3300 return format!("{home}/Library/Caches/petri");
3301 }
3302 }
3303
3304 #[cfg(windows)]
3305 {
3306 if let Ok(local) = std::env::var("LOCALAPPDATA") {
3307 return format!("{local}\\petri\\cache");
3308 }
3309 }
3310
3311 if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
3313 return format!("{xdg}/petri");
3314 }
3315 if let Ok(home) = std::env::var("HOME") {
3316 return format!("{home}/.cache/petri");
3317 }
3318
3319 ".cache/petri".to_string()
3320}
3321
3322#[cfg(test)]
3323mod tests {
3324 use super::make_vm_safe_name;
3325 use crate::Drive;
3326 use crate::VmbusStorageController;
3327 use crate::VmbusStorageType;
3328 use crate::Vtl;
3329
3330 #[test]
3331 fn test_short_names_unchanged() {
3332 let short_name = "short_test_name";
3333 assert_eq!(make_vm_safe_name(short_name), short_name);
3334 }
3335
3336 #[test]
3337 fn test_exactly_100_chars_unchanged() {
3338 let name_100 = "a".repeat(100);
3339 assert_eq!(make_vm_safe_name(&name_100), name_100);
3340 }
3341
3342 #[test]
3343 fn test_long_name_truncated() {
3344 let long_name = "multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_openhcl_servicing";
3345 let result = make_vm_safe_name(long_name);
3346
3347 assert_eq!(result.len(), 100);
3349
3350 assert!(result.starts_with("multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_ope"));
3352
3353 let suffix = &result[96..];
3355 assert_eq!(suffix.len(), 4);
3356 assert!(u16::from_str_radix(suffix, 16).is_ok());
3358 }
3359
3360 #[test]
3361 fn test_deterministic_results() {
3362 let long_name = "very_long_test_name_that_exceeds_the_100_character_limit_and_should_be_truncated_consistently_every_time";
3363 let result1 = make_vm_safe_name(long_name);
3364 let result2 = make_vm_safe_name(long_name);
3365
3366 assert_eq!(result1, result2);
3367 assert_eq!(result1.len(), 100);
3368 }
3369
3370 #[test]
3371 fn test_different_names_different_hashes() {
3372 let name1 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_1";
3373 let name2 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_2";
3374
3375 let result1 = make_vm_safe_name(name1);
3376 let result2 = make_vm_safe_name(name2);
3377
3378 assert_eq!(result1.len(), 100);
3380 assert_eq!(result2.len(), 100);
3381
3382 assert_ne!(result1, result2);
3384 assert_ne!(&result1[96..], &result2[96..]);
3385 }
3386
3387 #[test]
3388 fn test_vmbus_storage_controller() {
3389 let mut controller = VmbusStorageController::new(Vtl::Vtl0, VmbusStorageType::Scsi);
3390 assert_eq!(
3391 controller.set_drive(Some(1), Drive::new(None, false), false),
3392 1
3393 );
3394 assert!(controller.drives.contains_key(&1));
3395 assert_eq!(
3396 controller.set_drive(None, Drive::new(None, false), false),
3397 0
3398 );
3399 assert!(controller.drives.contains_key(&0));
3400 assert_eq!(
3401 controller.set_drive(None, Drive::new(None, false), false),
3402 2
3403 );
3404 assert!(controller.drives.contains_key(&2));
3405 assert_eq!(
3406 controller.set_drive(Some(0), Drive::new(None, false), true),
3407 0
3408 );
3409 assert!(controller.drives.contains_key(&0));
3410 }
3411}