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 no_vmbus: bool,
187}
188
189impl<T: PetriVmmBackend> Debug for PetriVmBuilder<T> {
190 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191 f.debug_struct("PetriVmBuilder")
192 .field("backend", &self.backend)
193 .field("config", &self.config)
194 .field("modify_vmm_config", &self.modify_vmm_config.is_some())
195 .field("resources", &self.resources)
196 .field("guest_quirks", &self.guest_quirks)
197 .field("vmm_quirks", &self.vmm_quirks)
198 .field("expected_boot_event", &self.expected_boot_event)
199 .field("override_expect_reset", &self.override_expect_reset)
200 .field("agent_image", &self.agent_image)
201 .field("openhcl_agent_image", &self.openhcl_agent_image)
202 .field("boot_device_type", &self.boot_device_type)
203 .field("minimal_mode", &self.minimal_mode)
204 .field("enable_serial", &self.enable_serial)
205 .field("enable_screenshots", &self.enable_screenshots)
206 .field("prebuilt_initrd", &self.prebuilt_initrd)
207 .field("use_virtio_vsock", &self.use_virtio_vsock)
208 .field("no_vmbus", &self.no_vmbus)
209 .finish()
210 }
211}
212
213#[derive(Debug)]
215pub struct PetriVmConfig {
216 pub name: String,
218 pub arch: MachineArch,
220 pub host_log_levels: Option<OpenvmmLogConfig>,
222 pub firmware: Firmware,
224 pub memory: MemoryConfig,
226 pub proc_topology: ProcessorTopology,
228 pub vmgs: PetriVmgsResource,
230 pub tpm: Option<TpmConfig>,
232 pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
234 pub pcie_nvme_drives: Vec<PcieNvmeDrive>,
236 pub physical_nvme_devices: HashMap<Guid, PhysicalNvmeDevice>,
238}
239
240#[derive(Debug)]
242pub struct PcieNvmeDrive {
243 pub port_name: String,
245 pub nsid: u32,
247 pub drive: Drive,
249}
250
251#[derive(Debug, Clone)]
254pub struct PhysicalNvmeDevice {
255 pub target_vtl: Vtl,
257 pub nsid: u32,
259 pub namespace_size_mib: u64,
261}
262
263pub struct PetriVmProperties {
266 pub is_openhcl: bool,
268 pub is_isolated: bool,
270 pub is_pcat: bool,
272 pub is_linux_direct: bool,
274 pub using_vtl0_pipette: bool,
276 pub using_vpci: bool,
278 pub os_flavor: OsFlavor,
280 pub minimal_mode: bool,
282 pub uses_pipette_as_init: bool,
284 pub enable_serial: bool,
286 pub prebuilt_initrd: Option<PathBuf>,
288 pub has_agent_disk: bool,
290 pub use_virtio_vsock: bool,
292 pub no_vmbus: bool,
294}
295
296pub struct PetriVmRuntimeConfig {
298 pub vtl2_settings: Option<Vtl2Settings>,
300 pub ide_controllers: Option<[[Option<Drive>; 2]; 2]>,
302 pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
304}
305
306#[derive(Debug)]
308pub struct PetriVmResources {
309 driver: DefaultDriver,
310 log_source: PetriLogSource,
311}
312
313#[async_trait]
315pub trait PetriVmmBackend: Debug {
316 type VmmConfig;
318
319 type VmRuntime: PetriVmRuntime;
321
322 fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool;
325
326 fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks);
328
329 fn default_servicing_flags() -> OpenHclServicingFlags;
331
332 fn create_guest_dump_disk() -> anyhow::Result<
335 Option<(
336 Arc<TempPath>,
337 Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
338 )>,
339 >;
340
341 fn new(resolver: &ArtifactResolver<'_>) -> Self;
343
344 async fn run(
346 self,
347 config: PetriVmConfig,
348 modify_vmm_config: Option<ModifyFn<Self::VmmConfig>>,
349 resources: &PetriVmResources,
350 properties: PetriVmProperties,
351 ) -> anyhow::Result<(Self::VmRuntime, PetriVmRuntimeConfig)>;
352}
353
354pub(crate) const PETRI_IDE_BOOT_CONTROLLER_NUMBER: u32 = 0;
356pub(crate) const PETRI_IDE_BOOT_LUN: u8 = 0;
357pub(crate) const PETRI_IDE_BOOT_CONTROLLER: Guid =
358 guid::guid!("ca56751f-e643-4bef-bf54-f73678e8b7b5");
359
360pub(crate) const PETRI_SCSI_BOOT_LUN: u32 = 0;
362pub(crate) const PETRI_SCSI_PIPETTE_LUN: u32 = 1;
363pub(crate) const PETRI_SCSI_CRASH_LUN: u32 = 2;
364pub(crate) const PETRI_SCSI_VTL0_CONTROLLER: Guid =
366 guid::guid!("27b553e8-8b39-411b-a55f-839971a7884f");
367pub(crate) const PETRI_SCSI_VTL2_CONTROLLER: Guid =
369 guid::guid!("766e96f8-2ceb-437e-afe3-a93169e48a7c");
370pub(crate) const PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER: Guid =
372 guid::guid!("6c474f47-ed39-49e6-bbb9-142177a1da6e");
373
374pub(crate) const PETRI_NVME_BOOT_NSID: u32 = 37;
376pub(crate) const PETRI_NVME_BOOT_VTL0_CONTROLLER: Guid =
378 guid::guid!("e23a04e2-90f5-4852-bc9d-e7ac691b756c");
379pub(crate) const PETRI_NVME_BOOT_VTL2_CONTROLLER: Guid =
381 guid::guid!("92bc8346-718b-449a-8751-edbf3dcd27e4");
382
383pub(crate) const PETRI_PCIE_NVME_AGENT_PORT: &str = "s0rc0rp1";
385pub(crate) const PETRI_PCIE_NVME_AGENT_NSID: u32 = 1;
387
388pub struct PetriVm<T: PetriVmmBackend> {
390 resources: PetriVmResources,
391 runtime: T::VmRuntime,
392 watchdog_tasks: Vec<Task<()>>,
393 openhcl_diag_handler: Option<OpenHclDiagHandler>,
394
395 arch: MachineArch,
396 guest_quirks: GuestQuirksInner,
397 vmm_quirks: VmmQuirks,
398 expected_boot_event: Option<FirmwareEvent>,
399
400 config: PetriVmRuntimeConfig,
401}
402
403impl<T: PetriVmmBackend> PetriVmBuilder<T> {
404 pub fn new(
406 params: PetriTestParams<'_>,
407 artifacts: PetriVmArtifacts<T>,
408 driver: &DefaultDriver,
409 ) -> anyhow::Result<Self> {
410 let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
411 let expected_boot_event = artifacts.firmware.expected_boot_event();
412 let boot_device_type = match artifacts.firmware {
413 Firmware::LinuxDirect { .. } => BootDeviceType::None,
414 Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
415 Firmware::Pcat { .. } => BootDeviceType::Ide,
416 Firmware::OpenhclPcat { .. } => BootDeviceType::IdeViaScsi,
417 Firmware::Uefi {
418 guest: UefiGuest::None,
419 ..
420 }
421 | Firmware::OpenhclUefi {
422 guest: UefiGuest::None,
423 ..
424 } => BootDeviceType::None,
425 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
426 };
427
428 Ok(Self {
429 backend: artifacts.backend,
430 config: PetriVmConfig {
431 name: make_vm_safe_name(params.test_name),
432 arch: artifacts.arch,
433 host_log_levels: None,
434 firmware: artifacts.firmware,
435 memory: Default::default(),
436 proc_topology: Default::default(),
437
438 vmgs: PetriVmgsResource::Ephemeral,
439 tpm: None,
440 vmbus_storage_controllers: HashMap::new(),
441 pcie_nvme_drives: Vec::new(),
442 physical_nvme_devices: HashMap::new(),
443 },
444 modify_vmm_config: None,
445 resources: PetriVmResources {
446 driver: driver.clone(),
447 log_source: params.logger.clone(),
448 },
449
450 guest_quirks,
451 vmm_quirks,
452 expected_boot_event,
453 override_expect_reset: false,
454
455 agent_image: artifacts.agent_image,
456 openhcl_agent_image: artifacts.openhcl_agent_image,
457 boot_device_type,
458
459 minimal_mode: false,
460 pipette_binary: artifacts.pipette_binary,
461 enable_serial: true,
462 enable_screenshots: true,
463 prebuilt_initrd: None,
464 use_virtio_vsock: false,
465 no_vmbus: false,
466 }
467 .add_petri_scsi_controllers()
468 .add_guest_crash_disk(params.post_test_hooks))
469 }
470
471 pub fn minimal(
482 params: PetriTestParams<'_>,
483 artifacts: PetriVmArtifacts<T>,
484 driver: &DefaultDriver,
485 ) -> anyhow::Result<Self> {
486 let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
487 let expected_boot_event = artifacts.firmware.expected_boot_event();
488 let boot_device_type = match artifacts.firmware {
489 Firmware::LinuxDirect { .. } => BootDeviceType::None,
490 Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
491 Firmware::Pcat { .. } => BootDeviceType::Ide,
492 Firmware::OpenhclPcat { .. } => BootDeviceType::IdeViaScsi,
493 Firmware::Uefi {
494 guest: UefiGuest::None,
495 ..
496 }
497 | Firmware::OpenhclUefi {
498 guest: UefiGuest::None,
499 ..
500 } => BootDeviceType::None,
501 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
502 };
503
504 Ok(Self {
505 backend: artifacts.backend,
506 config: PetriVmConfig {
507 name: make_vm_safe_name(params.test_name),
508 arch: artifacts.arch,
509 host_log_levels: None,
510 firmware: artifacts.firmware,
511 memory: Default::default(),
512 proc_topology: Default::default(),
513
514 vmgs: PetriVmgsResource::Ephemeral,
515 tpm: None,
516 vmbus_storage_controllers: HashMap::new(),
517 pcie_nvme_drives: Vec::new(),
518 physical_nvme_devices: HashMap::new(),
519 },
520 modify_vmm_config: None,
521 resources: PetriVmResources {
522 driver: driver.clone(),
523 log_source: params.logger.clone(),
524 },
525
526 guest_quirks,
527 vmm_quirks,
528 expected_boot_event,
529 override_expect_reset: false,
530
531 agent_image: artifacts.agent_image,
532 openhcl_agent_image: artifacts.openhcl_agent_image,
533 boot_device_type,
534
535 minimal_mode: true,
536 pipette_binary: artifacts.pipette_binary,
537 enable_serial: false,
538 enable_screenshots: true,
539 prebuilt_initrd: None,
540 use_virtio_vsock: false,
541 no_vmbus: false,
542 })
543 }
544
545 pub fn is_minimal(&self) -> bool {
547 self.minimal_mode
548 }
549
550 pub fn with_prebuilt_initrd(mut self, path: PathBuf) -> Self {
557 self.prebuilt_initrd = Some(path);
558 self
559 }
560
561 pub fn prepare_initrd(&self) -> anyhow::Result<TempPath> {
572 use anyhow::Context;
573 use std::io::Write;
574
575 let initrd_path = self
576 .config
577 .firmware
578 .linux_direct_initrd()
579 .context("prepare_initrd requires Linux direct boot with initrd")?;
580 let pipette_path = self
581 .pipette_binary
582 .as_ref()
583 .context("prepare_initrd requires a pipette binary")?;
584
585 let initrd_gz = std::fs::read(initrd_path)
586 .with_context(|| format!("failed to read initrd at {}", initrd_path.display()))?;
587 let pipette_data = std::fs::read(pipette_path.get()).with_context(|| {
588 format!(
589 "failed to read pipette binary at {}",
590 pipette_path.get().display()
591 )
592 })?;
593
594 let merged_gz =
595 initrd_cpio::inject_into_initrd(&initrd_gz, "pipette", &pipette_data, 0o100755)
596 .context("failed to inject pipette into initrd")?;
597
598 let mut tmp = tempfile::NamedTempFile::new()
599 .context("failed to create temp file for pre-built initrd")?;
600 tmp.write_all(&merged_gz)
601 .context("failed to write pre-built initrd")?;
602
603 Ok(tmp.into_temp_path())
604 }
605
606 pub fn with_serial_output(mut self) -> Self {
615 self.enable_serial = true;
616 self
617 }
618
619 pub fn without_serial_output(mut self) -> Self {
624 self.enable_serial = false;
625 self
626 }
627
628 pub fn without_screenshots(mut self) -> Self {
633 self.enable_screenshots = false;
634 self
635 }
636
637 pub fn with_virtio_vsock(mut self) -> Self {
648 self.use_virtio_vsock = true;
649 self
650 }
651
652 pub fn with_no_vmbus(mut self) -> Self {
660 self.no_vmbus = true;
661 if self.config.firmware.os_flavor() != OsFlavor::Windows {
662 self.use_virtio_vsock = true;
663 }
664 self.config.vmbus_storage_controllers.clear();
665 self
666 }
667
668 fn add_petri_scsi_controllers(self) -> Self {
669 let builder = self.add_vmbus_storage_controller(
670 &PETRI_SCSI_VTL0_CONTROLLER,
671 Vtl::Vtl0,
672 VmbusStorageType::Scsi,
673 );
674
675 if builder.is_openhcl() {
676 builder.add_vmbus_storage_controller(
677 &PETRI_SCSI_VTL2_CONTROLLER,
678 Vtl::Vtl2,
679 VmbusStorageType::Scsi,
680 )
681 } else {
682 builder
683 }
684 }
685
686 fn add_guest_crash_disk(self, post_test_hooks: &mut Vec<PetriPostTestHook>) -> Self {
687 let logger = self.resources.log_source.clone();
688 let (disk, disk_hook) = matches!(
689 self.config.firmware.os_flavor(),
690 OsFlavor::Windows | OsFlavor::Linux
691 )
692 .then(|| T::create_guest_dump_disk().expect("failed to create guest dump disk"))
693 .flatten()
694 .unzip();
695
696 if let Some(disk_hook) = disk_hook {
697 post_test_hooks.push(PetriPostTestHook::new(
698 "extract guest crash dumps".into(),
699 move |test_passed| {
700 if test_passed {
701 return Ok(());
702 }
703 let mut disk = disk_hook()?;
704 let gpt = gptman::GPT::read_from(&mut disk, SECTOR_SIZE)?;
705 let partition = fscommon::StreamSlice::new(
706 &mut disk,
707 gpt[1].starting_lba * SECTOR_SIZE,
708 gpt[1].ending_lba * SECTOR_SIZE,
709 )?;
710 let fs = fatfs::FileSystem::new(partition, fatfs::FsOptions::new())?;
711 for entry in fs.root_dir().iter() {
712 let Ok(entry) = entry else {
713 tracing::warn!(?entry, "failed to read entry in guest crash dump disk");
714 continue;
715 };
716 if !entry.is_file() {
717 tracing::warn!(
718 ?entry,
719 "skipping non-file entry in guest crash dump disk"
720 );
721 continue;
722 }
723 logger.write_attachment(&entry.file_name(), entry.to_file())?;
724 }
725 Ok(())
726 },
727 ));
728 }
729
730 if let Some(disk) = disk {
731 self.add_vmbus_drive(
732 Drive::new(Some(Disk::Temporary(disk)), false),
733 &PETRI_SCSI_VTL0_CONTROLLER,
734 Some(PETRI_SCSI_CRASH_LUN),
735 )
736 } else {
737 self
738 }
739 }
740
741 fn add_agent_disks(self) -> Self {
742 self.add_agent_disk_inner(Vtl::Vtl0)
743 .add_agent_disk_inner(Vtl::Vtl2)
744 }
745
746 fn add_agent_disk_inner(mut self, target_vtl: Vtl) -> Self {
747 let (agent_image, controller_id) = match target_vtl {
748 Vtl::Vtl0 => (self.agent_image.as_ref(), PETRI_SCSI_VTL0_CONTROLLER),
749 Vtl::Vtl1 => panic!("no VTL1 agent disk"),
750 Vtl::Vtl2 => (
751 self.openhcl_agent_image.as_ref(),
752 PETRI_SCSI_VTL2_CONTROLLER,
753 ),
754 };
755
756 if target_vtl == Vtl::Vtl0
759 && self.uses_pipette_as_init()
760 && !agent_image.is_some_and(|i| i.has_extras())
761 {
762 return self;
763 }
764
765 let Some(agent_disk) = agent_image.and_then(|i| {
766 i.build(crate::disk_image::ImageType::Vhd)
767 .expect("failed to build agent image")
768 }) else {
769 return self;
770 };
771
772 if self.no_vmbus {
775 self.config.pcie_nvme_drives.push(PcieNvmeDrive {
776 port_name: PETRI_PCIE_NVME_AGENT_PORT.into(),
777 nsid: PETRI_PCIE_NVME_AGENT_NSID,
778 drive: Drive::new(
779 Some(Disk::Temporary(Arc::new(agent_disk.into_temp_path()))),
780 false,
781 ),
782 });
783 return self;
784 }
785
786 if !self
789 .config
790 .vmbus_storage_controllers
791 .contains_key(&controller_id)
792 {
793 self = self.add_vmbus_storage_controller(
794 &controller_id,
795 target_vtl,
796 VmbusStorageType::Scsi,
797 );
798 }
799
800 self.add_vmbus_drive(
801 Drive::new(
802 Some(Disk::Temporary(Arc::new(agent_disk.into_temp_path()))),
803 false,
804 ),
805 &controller_id,
806 Some(PETRI_SCSI_PIPETTE_LUN),
807 )
808 }
809
810 fn add_boot_disk(mut self) -> Self {
811 if self.boot_device_type.requires_vtl2() && !self.is_openhcl() {
812 panic!("boot device type {:?} requires vtl2", self.boot_device_type);
813 }
814
815 if self.no_vmbus && self.boot_device_type.requires_vmbus() {
816 panic!(
817 "boot device type {:?} requires vmbus, but vmbus is disabled; \
818 use with_boot_device_type(BootDeviceType::PcieNvme) or similar",
819 self.boot_device_type
820 );
821 }
822
823 if self.boot_device_type.requires_vpci_boot() {
824 self.config
825 .firmware
826 .uefi_config_mut()
827 .expect("vpci boot requires uefi")
828 .enable_vpci_boot = true;
829 }
830
831 if let Some(boot_drive) = self.config.firmware.boot_drive() {
832 match self.boot_device_type {
833 BootDeviceType::None => unreachable!(),
834 BootDeviceType::Ide => self.add_ide_drive(
835 boot_drive,
836 PETRI_IDE_BOOT_CONTROLLER_NUMBER,
837 PETRI_IDE_BOOT_LUN,
838 ),
839 BootDeviceType::IdeViaScsi => self
840 .add_vmbus_drive(
841 boot_drive,
842 &PETRI_SCSI_VTL2_CONTROLLER,
843 Some(PETRI_SCSI_BOOT_LUN),
844 )
845 .add_vtl2_storage_controller(
846 Vtl2StorageControllerBuilder::new(ControllerType::Ide)
847 .with_instance_id(PETRI_IDE_BOOT_CONTROLLER)
848 .add_lun(
849 Vtl2LunBuilder::disk()
850 .with_channel(PETRI_IDE_BOOT_CONTROLLER_NUMBER)
851 .with_location(PETRI_IDE_BOOT_LUN as u32)
852 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
853 ControllerType::Scsi,
854 PETRI_SCSI_VTL2_CONTROLLER,
855 PETRI_SCSI_BOOT_LUN,
856 )),
857 )
858 .build(),
859 ),
860 BootDeviceType::IdeViaNvme => todo!(),
861 BootDeviceType::Scsi => self.add_vmbus_drive(
862 boot_drive,
863 &PETRI_SCSI_VTL0_CONTROLLER,
864 Some(PETRI_SCSI_BOOT_LUN),
865 ),
866 BootDeviceType::ScsiViaScsi => self
867 .add_vmbus_drive(
868 boot_drive,
869 &PETRI_SCSI_VTL2_CONTROLLER,
870 Some(PETRI_SCSI_BOOT_LUN),
871 )
872 .add_vtl2_storage_controller(
873 Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
874 .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
875 .add_lun(
876 Vtl2LunBuilder::disk()
877 .with_location(PETRI_SCSI_BOOT_LUN)
878 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
879 ControllerType::Scsi,
880 PETRI_SCSI_VTL2_CONTROLLER,
881 PETRI_SCSI_BOOT_LUN,
882 )),
883 )
884 .build(),
885 ),
886 BootDeviceType::ScsiViaNvme => self
887 .add_vmbus_storage_controller(
888 &PETRI_NVME_BOOT_VTL2_CONTROLLER,
889 Vtl::Vtl2,
890 VmbusStorageType::Nvme,
891 )
892 .add_vmbus_drive(
893 boot_drive,
894 &PETRI_NVME_BOOT_VTL2_CONTROLLER,
895 Some(PETRI_NVME_BOOT_NSID),
896 )
897 .add_vtl2_storage_controller(
898 Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
899 .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
900 .add_lun(
901 Vtl2LunBuilder::disk()
902 .with_location(PETRI_SCSI_BOOT_LUN)
903 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
904 ControllerType::Nvme,
905 PETRI_NVME_BOOT_VTL2_CONTROLLER,
906 PETRI_NVME_BOOT_NSID,
907 )),
908 )
909 .build(),
910 ),
911 BootDeviceType::Nvme => self
912 .add_vmbus_storage_controller(
913 &PETRI_NVME_BOOT_VTL0_CONTROLLER,
914 Vtl::Vtl0,
915 VmbusStorageType::Nvme,
916 )
917 .add_vmbus_drive(
918 boot_drive,
919 &PETRI_NVME_BOOT_VTL0_CONTROLLER,
920 Some(PETRI_NVME_BOOT_NSID),
921 ),
922 BootDeviceType::NvmeViaScsi => todo!(),
923 BootDeviceType::NvmeViaNvme => todo!(),
924 BootDeviceType::PcieNvme => {
925 self.config.pcie_nvme_drives.push(PcieNvmeDrive {
926 port_name: "s0rc0rp0".into(),
927 nsid: 1,
928 drive: boot_drive,
929 });
930 self
931 }
932 }
933 } else {
934 self
935 }
936 }
937
938 fn has_agent_disk(&self) -> bool {
943 if self.uses_pipette_as_init() {
944 self.agent_image.as_ref().is_some_and(|i| i.has_extras())
945 } else {
946 self.agent_image.is_some()
947 }
948 }
949
950 pub fn properties(&self) -> PetriVmProperties {
952 PetriVmProperties {
953 is_openhcl: self.config.firmware.is_openhcl(),
954 is_isolated: self.config.firmware.isolation().is_some(),
955 is_pcat: self.config.firmware.is_pcat(),
956 is_linux_direct: self.config.firmware.is_linux_direct(),
957 using_vtl0_pipette: self.using_vtl0_pipette(),
958 using_vpci: self.boot_device_type.requires_vpci_boot(),
959 os_flavor: self.config.firmware.os_flavor(),
960 minimal_mode: self.minimal_mode,
961 uses_pipette_as_init: self.uses_pipette_as_init(),
962 enable_serial: self.enable_serial,
963 prebuilt_initrd: self.prebuilt_initrd.clone(),
964 has_agent_disk: self.has_agent_disk(),
965 use_virtio_vsock: self.use_virtio_vsock,
966 no_vmbus: self.no_vmbus,
967 }
968 }
969
970 fn uses_pipette_as_init(&self) -> bool {
976 self.config.firmware.is_linux_direct()
977 && !self.config.firmware.is_openhcl()
978 && self.pipette_binary.is_some()
979 }
980
981 pub fn using_vtl0_pipette(&self) -> bool {
983 self.uses_pipette_as_init()
984 || self
985 .agent_image
986 .as_ref()
987 .is_some_and(|x| x.contains_pipette())
988 }
989
990 pub async fn run_without_agent(self) -> anyhow::Result<PetriVm<T>> {
994 self.run_core().await
995 }
996
997 pub async fn run(self) -> anyhow::Result<(PetriVm<T>, PipetteClient)> {
1000 assert!(self.using_vtl0_pipette());
1001
1002 let mut vm = self.run_core().await?;
1003 let client = vm.wait_for_agent().await?;
1004 Ok((vm, client))
1005 }
1006
1007 async fn run_core(mut self) -> anyhow::Result<PetriVm<T>> {
1008 self = self.add_boot_disk().add_agent_disks();
1011
1012 let _prepared_initrd_guard;
1016 if self.uses_pipette_as_init() && self.prebuilt_initrd.is_none() {
1017 let tmp = self.prepare_initrd()?;
1018 self.prebuilt_initrd = Some(tmp.to_path_buf());
1019 _prepared_initrd_guard = Some(tmp);
1020 } else {
1021 _prepared_initrd_guard = None;
1022 }
1023
1024 tracing::debug!(builder = ?self);
1025
1026 let arch = self.config.arch;
1027 let expect_reset = self.expect_reset();
1028 let properties = self.properties();
1029
1030 let (mut runtime, config) = self
1031 .backend
1032 .run(
1033 self.config,
1034 self.modify_vmm_config,
1035 &self.resources,
1036 properties,
1037 )
1038 .await?;
1039 let openhcl_diag_handler = runtime.openhcl_diag();
1040 let watchdog_tasks =
1041 Self::start_watchdog_tasks(&self.resources, &mut runtime, self.enable_screenshots)?;
1042
1043 let mut vm = PetriVm {
1044 resources: self.resources,
1045 runtime,
1046 watchdog_tasks,
1047 openhcl_diag_handler,
1048
1049 arch,
1050 guest_quirks: self.guest_quirks,
1051 vmm_quirks: self.vmm_quirks,
1052 expected_boot_event: self.expected_boot_event,
1053
1054 config,
1055 };
1056
1057 if expect_reset {
1058 vm.wait_for_reset_core().await?;
1059 }
1060
1061 vm.wait_for_expected_boot_event().await?;
1062
1063 Ok(vm)
1064 }
1065
1066 fn expect_reset(&self) -> bool {
1067 self.override_expect_reset
1068 || matches!(
1069 (
1070 self.guest_quirks.initial_reboot,
1071 self.expected_boot_event,
1072 &self.config.firmware,
1073 &self.config.tpm,
1074 ),
1075 (
1076 Some(InitialRebootCondition::Always),
1077 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
1078 _,
1079 _,
1080 ) | (
1081 Some(InitialRebootCondition::WithTpm),
1082 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
1083 _,
1084 Some(_),
1085 )
1086 )
1087 }
1088
1089 fn start_watchdog_tasks(
1090 resources: &PetriVmResources,
1091 runtime: &mut T::VmRuntime,
1092 enable_screenshots: bool,
1093 ) -> anyhow::Result<Vec<Task<()>>> {
1094 let mut tasks = Vec::new();
1095
1096 {
1097 const TIMEOUT_DURATION_MINUTES: u64 = 10;
1098 const TIMER_DURATION: Duration = Duration::from_secs(TIMEOUT_DURATION_MINUTES * 60);
1099 let log_source = resources.log_source.clone();
1100 let inspect_task =
1101 |name,
1102 driver: &DefaultDriver,
1103 inspect: std::pin::Pin<Box<dyn Future<Output = _> + Send>>| {
1104 driver.spawn(format!("petri-watchdog-inspect-{name}"), async move {
1105 if CancelContext::new()
1106 .with_timeout(Duration::from_secs(10))
1107 .until_cancelled(save_inspect(name, inspect, &log_source))
1108 .await
1109 .is_err()
1110 {
1111 tracing::warn!(name, "Failed to collect inspect data within timeout");
1112 }
1113 })
1114 };
1115
1116 let driver = resources.driver.clone();
1117 let vmm_inspector = runtime.inspector();
1118 let openhcl_diag_handler = runtime.openhcl_diag();
1119 tasks.push(resources.driver.spawn("timer-watchdog", async move {
1120 PolledTimer::new(&driver).sleep(TIMER_DURATION).await;
1121 tracing::warn!("Test timeout reached after {TIMEOUT_DURATION_MINUTES} minutes, collecting diagnostics.");
1122 let mut timeout_tasks = Vec::new();
1123 if let Some(inspector) = vmm_inspector {
1124 timeout_tasks.push(inspect_task.clone()("vmm", &driver, Box::pin(async move { inspector.inspect_all().await })) );
1125 }
1126 if let Some(openhcl_diag_handler) = openhcl_diag_handler {
1127 timeout_tasks.push(inspect_task("openhcl", &driver, Box::pin(async move { openhcl_diag_handler.inspect("", None, None).await })));
1128 }
1129 futures::future::join_all(timeout_tasks).await;
1130 tracing::error!("Test time out diagnostics collection complete, aborting.");
1131 panic!("Test timed out");
1132 }));
1133 }
1134
1135 if enable_screenshots {
1136 if let Some(mut framebuffer_access) = runtime.take_framebuffer_access() {
1137 let mut timer = PolledTimer::new(&resources.driver);
1138 let log_source = resources.log_source.clone();
1139
1140 tasks.push(
1141 resources
1142 .driver
1143 .spawn("petri-watchdog-screenshot", async move {
1144 let mut image = Vec::new();
1145 let mut last_image = Vec::new();
1146 loop {
1147 timer.sleep(Duration::from_secs(2)).await;
1148 tracing::trace!("Taking screenshot.");
1149
1150 let VmScreenshotMeta {
1151 color,
1152 width,
1153 height,
1154 } = match framebuffer_access.screenshot(&mut image).await {
1155 Ok(Some(meta)) => meta,
1156 Ok(None) => {
1157 tracing::debug!("VM off, skipping screenshot.");
1158 continue;
1159 }
1160 Err(e) => {
1161 tracing::error!(?e, "Failed to take screenshot");
1162 continue;
1163 }
1164 };
1165
1166 if image == last_image {
1167 tracing::debug!(
1168 "No change in framebuffer, skipping screenshot."
1169 );
1170 continue;
1171 }
1172
1173 let r = log_source.create_attachment("screenshot.png").and_then(
1174 |mut f| {
1175 image::write_buffer_with_format(
1176 &mut f,
1177 &image,
1178 width.into(),
1179 height.into(),
1180 color,
1181 image::ImageFormat::Png,
1182 )
1183 .map_err(Into::into)
1184 },
1185 );
1186
1187 if let Err(e) = r {
1188 tracing::error!(?e, "Failed to save screenshot");
1189 } else {
1190 tracing::info!("Screenshot saved.");
1191 }
1192
1193 std::mem::swap(&mut image, &mut last_image);
1194 }
1195 }),
1196 );
1197 }
1198 }
1199
1200 Ok(tasks)
1201 }
1202
1203 pub fn with_expect_boot_failure(mut self) -> Self {
1206 self.expected_boot_event = Some(FirmwareEvent::BootFailed);
1207 self
1208 }
1209
1210 pub fn with_expect_no_boot_event(mut self) -> Self {
1213 self.expected_boot_event = None;
1214 self
1215 }
1216
1217 pub fn with_expect_reset(mut self) -> Self {
1221 self.override_expect_reset = true;
1222 self
1223 }
1224
1225 pub fn with_secure_boot(mut self) -> Self {
1227 self.config
1228 .firmware
1229 .uefi_config_mut()
1230 .expect("Secure boot is only supported for UEFI firmware.")
1231 .secure_boot_enabled = true;
1232
1233 match self.os_flavor() {
1234 OsFlavor::Windows => self.with_windows_secure_boot_template(),
1235 OsFlavor::Linux => self.with_uefi_ca_secure_boot_template(),
1236 _ => panic!(
1237 "Secure boot unsupported for OS flavor {:?}",
1238 self.os_flavor()
1239 ),
1240 }
1241 }
1242
1243 pub fn with_windows_secure_boot_template(mut self) -> Self {
1245 self.config
1246 .firmware
1247 .uefi_config_mut()
1248 .expect("Secure boot is only supported for UEFI firmware.")
1249 .secure_boot_template = Some(SecureBootTemplate::MicrosoftWindows);
1250 self
1251 }
1252
1253 pub fn with_uefi_ca_secure_boot_template(mut self) -> Self {
1255 self.config
1256 .firmware
1257 .uefi_config_mut()
1258 .expect("Secure boot is only supported for UEFI firmware.")
1259 .secure_boot_template = Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority);
1260 self
1261 }
1262
1263 pub fn with_processor_topology(mut self, topology: ProcessorTopology) -> Self {
1265 self.config.proc_topology = topology;
1266 self
1267 }
1268
1269 pub fn with_memory(mut self, memory: MemoryConfig) -> Self {
1271 self.config.memory = memory;
1272 self
1273 }
1274
1275 pub fn with_vtl2_base_address_type(mut self, address_type: Vtl2BaseAddressType) -> Self {
1280 self.config
1281 .firmware
1282 .openhcl_config_mut()
1283 .expect("OpenHCL firmware is required to set custom VTL2 address type.")
1284 .vtl2_base_address_type = Some(address_type);
1285 self
1286 }
1287
1288 pub fn with_custom_openhcl(mut self, artifact: ResolvedArtifact<impl IsOpenhclIgvm>) -> Self {
1290 match &mut self.config.firmware {
1291 Firmware::OpenhclLinuxDirect { igvm_path, .. }
1292 | Firmware::OpenhclPcat { igvm_path, .. }
1293 | Firmware::OpenhclUefi { igvm_path, .. } => {
1294 *igvm_path = artifact.erase();
1295 }
1296 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } | Firmware::Pcat { .. } => {
1297 panic!("Custom OpenHCL is only supported for OpenHCL firmware.")
1298 }
1299 }
1300 self
1301 }
1302
1303 pub fn with_openhcl_command_line(mut self, additional_command_line: &str) -> Self {
1305 append_cmdline(
1306 &mut self
1307 .config
1308 .firmware
1309 .openhcl_config_mut()
1310 .expect("OpenHCL command line is only supported for OpenHCL firmware.")
1311 .custom_command_line,
1312 additional_command_line,
1313 );
1314 self
1315 }
1316
1317 pub fn with_confidential_filtering(self) -> Self {
1319 if !self.config.firmware.is_openhcl() {
1320 panic!("Confidential filtering is only supported for OpenHCL");
1321 }
1322 self.with_openhcl_command_line(&format!(
1323 "{}=1 {}=0",
1324 underhill_confidentiality::OPENHCL_CONFIDENTIAL_ENV_VAR_NAME,
1325 underhill_confidentiality::OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME
1326 ))
1327 }
1328
1329 pub fn with_openhcl_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1331 self.config
1332 .firmware
1333 .openhcl_config_mut()
1334 .expect("OpenHCL firmware is required to set custom OpenHCL log levels.")
1335 .log_levels = levels;
1336 self
1337 }
1338
1339 pub fn with_host_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1343 if let OpenvmmLogConfig::Custom(ref custom_levels) = levels {
1344 for key in custom_levels.keys() {
1345 if !["OPENVMM_LOG", "OPENVMM_SHOW_SPANS"].contains(&key.as_str()) {
1346 panic!("Unsupported OpenVMM log level key: {}", key);
1347 }
1348 }
1349 }
1350
1351 self.config.host_log_levels = Some(levels.clone());
1352 self
1353 }
1354
1355 pub fn with_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
1357 self.agent_image
1358 .as_mut()
1359 .expect("no guest pipette")
1360 .add_file(name, artifact);
1361 self
1362 }
1363
1364 pub fn with_openhcl_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
1366 self.openhcl_agent_image
1367 .as_mut()
1368 .expect("no openhcl pipette")
1369 .add_file(name, artifact);
1370 self
1371 }
1372
1373 pub fn with_uefi_frontpage(mut self, enable: bool) -> Self {
1375 self.config
1376 .firmware
1377 .uefi_config_mut()
1378 .expect("UEFI frontpage is only supported for UEFI firmware.")
1379 .disable_frontpage = !enable;
1380 self
1381 }
1382
1383 pub fn with_efi_diagnostics_log_level(mut self, level: EfiDiagnosticsLogLevel) -> Self {
1389 self.config
1390 .firmware
1391 .uefi_config_mut()
1392 .expect("EFI diagnostics log level is only supported for UEFI firmware.")
1393 .efi_diagnostics_log_level = level;
1394 self
1395 }
1396
1397 pub fn with_efi_diagnostics_rate_limit(mut self, limit: u32) -> Self {
1403 self.config
1404 .firmware
1405 .uefi_config_mut()
1406 .expect("EFI diagnostics rate limit is only supported for UEFI firmware.")
1407 .efi_diagnostics_rate_limit = Some(limit);
1408 self
1409 }
1410
1411 pub fn with_default_boot_always_attempt(mut self, enable: bool) -> Self {
1413 self.config
1414 .firmware
1415 .uefi_config_mut()
1416 .expect("Default boot always attempt is only supported for UEFI firmware.")
1417 .default_boot_always_attempt = enable;
1418 self
1419 }
1420
1421 pub fn with_uefi_force_dma_bounce(mut self, enable: bool) -> Self {
1423 self.config
1424 .firmware
1425 .uefi_config_mut()
1426 .expect("force DMA bounce is only supported for UEFI firmware.")
1427 .force_dma_bounce = enable;
1428 self
1429 }
1430
1431 pub fn with_vmbus_redirect(mut self, enable: bool) -> Self {
1433 self.config
1434 .firmware
1435 .openhcl_config_mut()
1436 .expect("VMBus redirection is only supported for OpenHCL firmware.")
1437 .vmbus_redirect = enable;
1438 self
1439 }
1440
1441 pub fn with_guest_state_lifetime(
1443 mut self,
1444 guest_state_lifetime: PetriGuestStateLifetime,
1445 ) -> Self {
1446 let disk = match self.config.vmgs {
1447 PetriVmgsResource::Disk(disk)
1448 | PetriVmgsResource::ReprovisionOnFailure(disk)
1449 | PetriVmgsResource::Reprovision(disk) => disk,
1450 PetriVmgsResource::Ephemeral => PetriVmgsDisk::default(),
1451 };
1452 self.config.vmgs = match guest_state_lifetime {
1453 PetriGuestStateLifetime::Disk => PetriVmgsResource::Disk(disk),
1454 PetriGuestStateLifetime::ReprovisionOnFailure => {
1455 PetriVmgsResource::ReprovisionOnFailure(disk)
1456 }
1457 PetriGuestStateLifetime::Reprovision => PetriVmgsResource::Reprovision(disk),
1458 PetriGuestStateLifetime::Ephemeral => {
1459 if !matches!(disk.disk, Disk::Memory(_)) {
1460 panic!("attempted to use ephemeral guest state after specifying backing vmgs")
1461 }
1462 PetriVmgsResource::Ephemeral
1463 }
1464 };
1465 self
1466 }
1467
1468 pub fn with_guest_state_encryption(mut self, policy: GuestStateEncryptionPolicy) -> Self {
1470 match &mut self.config.vmgs {
1471 PetriVmgsResource::Disk(vmgs)
1472 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1473 | PetriVmgsResource::Reprovision(vmgs) => {
1474 vmgs.encryption_policy = policy;
1475 }
1476 PetriVmgsResource::Ephemeral => {
1477 panic!("attempted to encrypt ephemeral guest state")
1478 }
1479 }
1480 self
1481 }
1482
1483 pub fn with_initial_vmgs(self, disk: ResolvedArtifact<impl IsTestVmgs>) -> Self {
1485 self.with_backing_vmgs(Disk::Differencing(DiskPath::Local(disk.into())))
1486 }
1487
1488 pub fn with_persistent_vmgs(self, disk: impl AsRef<Path>) -> Self {
1490 self.with_backing_vmgs(Disk::Persistent(disk.as_ref().to_path_buf()))
1491 }
1492
1493 fn with_backing_vmgs(mut self, disk: Disk) -> Self {
1494 match &mut self.config.vmgs {
1495 PetriVmgsResource::Disk(vmgs)
1496 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1497 | PetriVmgsResource::Reprovision(vmgs) => {
1498 if !matches!(vmgs.disk, Disk::Memory(_)) {
1499 panic!("already specified a backing vmgs file");
1500 }
1501 vmgs.disk = disk;
1502 }
1503 PetriVmgsResource::Ephemeral => {
1504 panic!("attempted to specify a backing vmgs with ephemeral guest state")
1505 }
1506 }
1507 self
1508 }
1509
1510 pub fn with_boot_device_type(mut self, boot: BootDeviceType) -> Self {
1514 self.boot_device_type = boot;
1515 self
1516 }
1517
1518 pub fn with_tpm(mut self, enable: bool) -> Self {
1520 if enable {
1521 self.config.tpm.get_or_insert_default();
1522 } else {
1523 self.config.tpm = None;
1524 }
1525 self
1526 }
1527
1528 pub fn with_tpm_state_persistence(mut self, tpm_state_persistence: bool) -> Self {
1530 self.config
1531 .tpm
1532 .as_mut()
1533 .expect("TPM persistence requires a TPM")
1534 .no_persistent_secrets = !tpm_state_persistence;
1535 self
1536 }
1537
1538 pub fn with_custom_vtl2_settings(
1542 mut self,
1543 f: impl FnOnce(&mut Vtl2Settings) + 'static + Send + Sync,
1544 ) -> Self {
1545 f(self
1546 .config
1547 .firmware
1548 .vtl2_settings()
1549 .expect("Custom VTL 2 settings are only supported with OpenHCL"));
1550 self
1551 }
1552
1553 pub fn add_vtl2_storage_controller(self, controller: StorageController) -> Self {
1555 self.with_custom_vtl2_settings(move |v| {
1556 v.dynamic
1557 .as_mut()
1558 .unwrap()
1559 .storage_controllers
1560 .push(controller)
1561 })
1562 }
1563
1564 pub fn add_vmbus_storage_controller(
1566 mut self,
1567 id: &Guid,
1568 target_vtl: Vtl,
1569 controller_type: VmbusStorageType,
1570 ) -> Self {
1571 if self
1572 .config
1573 .vmbus_storage_controllers
1574 .insert(
1575 *id,
1576 VmbusStorageController::new(target_vtl, controller_type),
1577 )
1578 .is_some()
1579 {
1580 panic!("storage controller {id} already existed");
1581 }
1582 self
1583 }
1584
1585 pub fn add_vmbus_drive(
1587 mut self,
1588 drive: Drive,
1589 controller_id: &Guid,
1590 controller_location: Option<u32>,
1591 ) -> Self {
1592 let controller = self
1593 .config
1594 .vmbus_storage_controllers
1595 .get_mut(controller_id)
1596 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
1597
1598 _ = controller.set_drive(controller_location, drive, false);
1599
1600 self
1601 }
1602
1603 pub fn add_ide_drive(
1605 mut self,
1606 drive: Drive,
1607 controller_number: u32,
1608 controller_location: u8,
1609 ) -> Self {
1610 self.config
1611 .firmware
1612 .ide_controllers_mut()
1613 .expect("Host IDE requires PCAT with no HCL")[controller_number as usize]
1614 [controller_location as usize] = Some(drive);
1615
1616 self
1617 }
1618
1619 pub fn add_physical_nvme_device(mut self, vsid: Guid, device: PhysicalNvmeDevice) -> Self {
1621 if self
1622 .config
1623 .physical_nvme_devices
1624 .insert(vsid, device)
1625 .is_some()
1626 {
1627 panic!("physical NVMe device {vsid} already existed");
1628 }
1629 self
1630 }
1631
1632 pub fn os_flavor(&self) -> OsFlavor {
1634 self.config.firmware.os_flavor()
1635 }
1636
1637 pub fn is_openhcl(&self) -> bool {
1639 self.config.firmware.is_openhcl()
1640 }
1641
1642 pub fn isolation(&self) -> Option<IsolationType> {
1644 self.config.firmware.isolation()
1645 }
1646
1647 pub fn arch(&self) -> MachineArch {
1649 self.config.arch
1650 }
1651
1652 pub fn log_source(&self) -> &PetriLogSource {
1654 &self.resources.log_source
1655 }
1656
1657 pub fn default_servicing_flags(&self) -> OpenHclServicingFlags {
1659 T::default_servicing_flags()
1660 }
1661
1662 pub fn modify_backend(
1664 mut self,
1665 f: impl FnOnce(T::VmmConfig) -> T::VmmConfig + 'static + Send,
1666 ) -> Self {
1667 if self.modify_vmm_config.is_some() {
1668 panic!("only one modify_backend allowed");
1669 }
1670 self.modify_vmm_config = Some(ModifyFn(Box::new(f)));
1671 self
1672 }
1673}
1674
1675impl<T: PetriVmmBackend> PetriVm<T> {
1676 pub async fn teardown(self) -> anyhow::Result<()> {
1678 tracing::info!("Tearing down VM...");
1679 self.runtime.teardown().await
1680 }
1681
1682 pub async fn wait_for_halt(&mut self) -> anyhow::Result<PetriHaltReasonDetail> {
1684 tracing::info!("Waiting for VM to halt...");
1685 let halt_reason = self.runtime.wait_for_halt(false).await?;
1686 tracing::info!("VM halted: {halt_reason:?}. Cancelling watchdogs...");
1687 futures::future::join_all(self.watchdog_tasks.drain(..).map(|t| t.cancel())).await;
1688 Ok(halt_reason)
1689 }
1690
1691 pub async fn wait_for_clean_shutdown(&mut self) -> anyhow::Result<()> {
1693 let halt_reason = self.wait_for_halt().await?;
1694 if halt_reason.reason != PetriHaltReason::PowerOff {
1695 anyhow::bail!("Expected PowerOff, got {halt_reason:?}");
1696 }
1697 tracing::info!("VM was cleanly powered off and torn down.");
1698 Ok(())
1699 }
1700
1701 pub async fn wait_for_teardown(mut self) -> anyhow::Result<PetriHaltReasonDetail> {
1704 let halt_reason = self.wait_for_halt().await?;
1705 self.teardown().await?;
1706 Ok(halt_reason)
1707 }
1708
1709 pub async fn wait_for_clean_teardown(mut self) -> anyhow::Result<()> {
1711 self.wait_for_clean_shutdown().await?;
1712 self.teardown().await
1713 }
1714
1715 pub async fn wait_for_reset_no_agent(&mut self) -> anyhow::Result<()> {
1717 self.wait_for_reset_core().await?;
1718 self.wait_for_expected_boot_event().await?;
1719 Ok(())
1720 }
1721
1722 pub async fn wait_for_reset(&mut self) -> anyhow::Result<PipetteClient> {
1724 self.wait_for_reset_no_agent().await?;
1725 self.wait_for_agent().await
1726 }
1727
1728 async fn wait_for_reset_core(&mut self) -> anyhow::Result<()> {
1729 tracing::info!("Waiting for VM to reset...");
1730 let halt_reason = self.runtime.wait_for_halt(true).await?;
1731 if halt_reason.reason != PetriHaltReason::Reset {
1732 anyhow::bail!("Expected reset, got {halt_reason:?}");
1733 }
1734 tracing::info!("VM reset.");
1735 Ok(())
1736 }
1737
1738 pub async fn inspect_openhcl(
1749 &self,
1750 path: impl Into<String>,
1751 depth: Option<usize>,
1752 timeout: Option<Duration>,
1753 ) -> anyhow::Result<inspect::Node> {
1754 self.openhcl_diag()?
1755 .inspect(path.into().as_str(), depth, timeout)
1756 .await
1757 }
1758
1759 pub async fn inspect_update_openhcl(
1769 &self,
1770 path: impl Into<String>,
1771 value: impl Into<String>,
1772 ) -> anyhow::Result<inspect::Value> {
1773 self.openhcl_diag()?
1774 .inspect_update(path.into(), value.into())
1775 .await
1776 }
1777
1778 pub async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()> {
1780 self.inspect_openhcl("", None, None).await.map(|_| ())
1781 }
1782
1783 pub async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()> {
1789 self.openhcl_diag()?.wait_for_vtl2().await
1790 }
1791
1792 pub async fn kmsg(&self) -> anyhow::Result<diag_client::kmsg_stream::KmsgStream> {
1794 self.openhcl_diag()?.kmsg().await
1795 }
1796
1797 pub async fn openhcl_core_dump(&self, name: &str, path: &Path) -> anyhow::Result<()> {
1800 self.openhcl_diag()?.core_dump(name, path).await
1801 }
1802
1803 pub async fn openhcl_crash(&self, name: &str) -> anyhow::Result<()> {
1805 self.openhcl_diag()?.crash(name).await
1806 }
1807
1808 async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient> {
1811 self.runtime.wait_for_enlightened_shutdown_ready().await?;
1821 self.runtime.wait_for_agent(false).await
1822 }
1823
1824 pub async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient> {
1828 self.launch_vtl2_pipette().await?;
1830 self.runtime.wait_for_agent(true).await
1831 }
1832
1833 async fn wait_for_expected_boot_event(&mut self) -> anyhow::Result<()> {
1840 if let Some(expected_event) = self.expected_boot_event {
1841 let event = self.wait_for_boot_event().await?;
1842
1843 anyhow::ensure!(
1844 event == expected_event,
1845 "Did not receive expected boot event"
1846 );
1847 } else {
1848 tracing::warn!("Boot event not emitted for configured firmware or manually ignored.");
1849 }
1850
1851 Ok(())
1852 }
1853
1854 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
1857 tracing::info!("Waiting for boot event...");
1858 let boot_event = loop {
1859 match CancelContext::new()
1860 .with_timeout(self.vmm_quirks.flaky_boot.unwrap_or(Duration::MAX))
1861 .until_cancelled(self.runtime.wait_for_boot_event())
1862 .await
1863 {
1864 Ok(res) => break res?,
1865 Err(_) => {
1866 tracing::error!("Did not get boot event in required time, resetting...");
1867 if let Some(inspector) = self.runtime.inspector() {
1868 save_inspect(
1869 "vmm",
1870 Box::pin(async move { inspector.inspect_all().await }),
1871 &self.resources.log_source,
1872 )
1873 .await;
1874 }
1875
1876 self.runtime.reset().await?;
1877 continue;
1878 }
1879 }
1880 };
1881 tracing::info!("Got boot event: {boot_event:?}");
1882 Ok(boot_event)
1883 }
1884
1885 pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
1888 tracing::info!("Waiting for enlightened shutdown to be ready");
1889 self.runtime.wait_for_enlightened_shutdown_ready().await?;
1890
1891 let mut wait_time = Duration::from_secs(10);
1897
1898 if let Some(duration) = self.guest_quirks.hyperv_shutdown_ic_sleep {
1900 wait_time += duration;
1901 }
1902
1903 tracing::info!(
1904 "Shutdown IC reported ready, waiting for an extra {}s",
1905 wait_time.as_secs()
1906 );
1907 PolledTimer::new(&self.resources.driver)
1908 .sleep(wait_time)
1909 .await;
1910
1911 tracing::info!("Sending enlightened shutdown command");
1912 self.runtime.send_enlightened_shutdown(kind).await
1913 }
1914
1915 pub async fn restart_openhcl(
1918 &mut self,
1919 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1920 flags: OpenHclServicingFlags,
1921 ) -> anyhow::Result<()> {
1922 self.runtime
1923 .restart_openhcl(&new_openhcl.erase(), flags)
1924 .await
1925 }
1926
1927 pub async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()> {
1930 self.runtime.update_command_line(command_line).await
1931 }
1932
1933 pub async fn add_pcie_device(
1935 &mut self,
1936 port_name: String,
1937 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
1938 ) -> anyhow::Result<()> {
1939 self.runtime.add_pcie_device(port_name, resource).await
1940 }
1941
1942 pub async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
1944 self.runtime.remove_pcie_device(port_name).await
1945 }
1946
1947 pub async fn save_openhcl(
1950 &mut self,
1951 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1952 flags: OpenHclServicingFlags,
1953 ) -> anyhow::Result<()> {
1954 self.runtime.save_openhcl(&new_openhcl.erase(), flags).await
1955 }
1956
1957 pub async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
1960 self.runtime.restore_openhcl().await
1961 }
1962
1963 pub fn arch(&self) -> MachineArch {
1965 self.arch
1966 }
1967
1968 pub fn backend(&mut self) -> &mut T::VmRuntime {
1970 &mut self.runtime
1971 }
1972
1973 async fn launch_vtl2_pipette(&self) -> anyhow::Result<()> {
1974 tracing::debug!("Launching VTL 2 pipette...");
1975
1976 let res = self
1978 .openhcl_diag()?
1979 .run_vtl2_command("sh", &["-c", "mkdir /cidata && mount LABEL=cidata /cidata"])
1980 .await?;
1981
1982 if !res.exit_status.success() {
1983 anyhow::bail!("Failed to mount VTL 2 pipette drive: {:?}", res);
1984 }
1985
1986 let res = self
1987 .openhcl_diag()?
1988 .run_detached_vtl2_command("sh", &["-c", "/cidata/pipette 2>&1 | logger &"])
1989 .await?;
1990
1991 if !res.success() {
1992 anyhow::bail!("Failed to spawn VTL 2 pipette: {:?}", res);
1993 }
1994
1995 Ok(())
1996 }
1997
1998 fn openhcl_diag(&self) -> anyhow::Result<&OpenHclDiagHandler> {
1999 if let Some(ohd) = self.openhcl_diag_handler.as_ref() {
2000 Ok(ohd)
2001 } else {
2002 anyhow::bail!("VM is not configured with OpenHCL")
2003 }
2004 }
2005
2006 pub async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2008 self.runtime.get_guest_state_file().await
2009 }
2010
2011 pub async fn modify_vtl2_settings(
2013 &mut self,
2014 f: impl FnOnce(&mut Vtl2Settings),
2015 ) -> anyhow::Result<()> {
2016 if self.openhcl_diag_handler.is_none() {
2017 panic!("Custom VTL 2 settings are only supported with OpenHCL");
2018 }
2019 f(self
2020 .config
2021 .vtl2_settings
2022 .get_or_insert_with(default_vtl2_settings));
2023 self.runtime
2024 .set_vtl2_settings(self.config.vtl2_settings.as_ref().unwrap())
2025 .await
2026 }
2027
2028 pub fn get_vmbus_storage_controllers(&self) -> &HashMap<Guid, VmbusStorageController> {
2030 &self.config.vmbus_storage_controllers
2031 }
2032
2033 pub async fn set_vmbus_drive(
2035 &mut self,
2036 drive: Drive,
2037 controller_id: &Guid,
2038 controller_location: Option<u32>,
2039 ) -> anyhow::Result<()> {
2040 let controller = self
2041 .config
2042 .vmbus_storage_controllers
2043 .get_mut(controller_id)
2044 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
2045
2046 let controller_location = controller.set_drive(controller_location, drive, true);
2047 let disk = controller.drives.get(&controller_location).unwrap();
2048
2049 self.runtime
2050 .set_vmbus_drive(disk, controller_id, controller_location)
2051 .await?;
2052
2053 Ok(())
2054 }
2055}
2056
2057#[async_trait]
2059pub trait PetriVmRuntime: Send + Sync + 'static {
2060 type VmInspector: PetriVmInspector;
2062 type VmFramebufferAccess: PetriVmFramebufferAccess;
2064
2065 async fn teardown(self) -> anyhow::Result<()>;
2067 async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReasonDetail>;
2070 async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>;
2072 fn openhcl_diag(&self) -> Option<OpenHclDiagHandler>;
2074 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent>;
2077 async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()>;
2080 async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
2082 async fn restart_openhcl(
2085 &mut self,
2086 new_openhcl: &ResolvedArtifact,
2087 flags: OpenHclServicingFlags,
2088 ) -> anyhow::Result<()>;
2089 async fn save_openhcl(
2093 &mut self,
2094 new_openhcl: &ResolvedArtifact,
2095 flags: OpenHclServicingFlags,
2096 ) -> anyhow::Result<()>;
2097 async fn restore_openhcl(&mut self) -> anyhow::Result<()>;
2100 async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()>;
2103 fn inspector(&self) -> Option<Self::VmInspector> {
2105 None
2106 }
2107 fn take_framebuffer_access(&mut self) -> Option<Self::VmFramebufferAccess> {
2110 None
2111 }
2112 async fn reset(&mut self) -> anyhow::Result<()>;
2114 async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2116 Ok(None)
2117 }
2118 async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()>;
2120 async fn set_vmbus_drive(
2122 &mut self,
2123 disk: &Drive,
2124 controller_id: &Guid,
2125 controller_location: u32,
2126 ) -> anyhow::Result<()>;
2127 async fn add_pcie_device(
2129 &mut self,
2130 port_name: String,
2131 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
2132 ) -> anyhow::Result<()> {
2133 let _ = (port_name, resource);
2134 anyhow::bail!("PCIe hotplug not supported by this backend")
2135 }
2136 async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
2138 let _ = port_name;
2139 anyhow::bail!("PCIe hotplug not supported by this backend")
2140 }
2141}
2142
2143#[async_trait]
2145pub trait PetriVmInspector: Send + Sync + 'static {
2146 async fn inspect_all(&self) -> anyhow::Result<inspect::Node>;
2148}
2149
2150pub struct NoPetriVmInspector;
2152#[async_trait]
2153impl PetriVmInspector for NoPetriVmInspector {
2154 async fn inspect_all(&self) -> anyhow::Result<inspect::Node> {
2155 unreachable!()
2156 }
2157}
2158
2159pub struct VmScreenshotMeta {
2161 pub color: image::ExtendedColorType,
2163 pub width: u16,
2165 pub height: u16,
2167}
2168
2169#[async_trait]
2171pub trait PetriVmFramebufferAccess: Send + 'static {
2172 async fn screenshot(&mut self, image: &mut Vec<u8>)
2175 -> anyhow::Result<Option<VmScreenshotMeta>>;
2176}
2177
2178pub struct NoPetriVmFramebufferAccess;
2180#[async_trait]
2181impl PetriVmFramebufferAccess for NoPetriVmFramebufferAccess {
2182 async fn screenshot(
2183 &mut self,
2184 _image: &mut Vec<u8>,
2185 ) -> anyhow::Result<Option<VmScreenshotMeta>> {
2186 unreachable!()
2187 }
2188}
2189
2190#[derive(Debug)]
2192pub struct ProcessorTopology {
2193 pub vp_count: u32,
2195 pub enable_smt: Option<bool>,
2197 pub vps_per_socket: Option<u32>,
2199 pub apic_mode: Option<ApicMode>,
2201}
2202
2203impl Default for ProcessorTopology {
2204 fn default() -> Self {
2205 Self {
2206 vp_count: 2,
2207 enable_smt: None,
2208 vps_per_socket: None,
2209 apic_mode: None,
2210 }
2211 }
2212}
2213
2214impl ProcessorTopology {
2215 pub fn heavy() -> Self {
2217 Self {
2218 vp_count: 16,
2219 vps_per_socket: Some(8),
2220 ..Default::default()
2221 }
2222 }
2223
2224 pub fn very_heavy() -> Self {
2226 Self {
2227 vp_count: 32,
2228 vps_per_socket: Some(16),
2229 ..Default::default()
2230 }
2231 }
2232}
2233
2234#[derive(Debug, Clone, Copy)]
2236pub enum ApicMode {
2237 Xapic,
2239 X2apicSupported,
2241 X2apicEnabled,
2243}
2244
2245#[derive(Debug)]
2247pub struct MemoryConfig {
2248 pub startup_bytes: u64,
2251 pub dynamic_memory_range: Option<(u64, u64)>,
2255 pub numa_mem_sizes: Option<Vec<u64>>,
2258}
2259
2260impl Default for MemoryConfig {
2261 fn default() -> Self {
2262 Self {
2263 startup_bytes: 4 * 1024 * 1024 * 1024, dynamic_memory_range: None,
2265 numa_mem_sizes: None,
2266 }
2267 }
2268}
2269
2270#[derive(Debug)]
2272pub struct UefiConfig {
2273 pub secure_boot_enabled: bool,
2275 pub secure_boot_template: Option<SecureBootTemplate>,
2277 pub disable_frontpage: bool,
2279 pub default_boot_always_attempt: bool,
2281 pub enable_vpci_boot: bool,
2283 pub force_dma_bounce: bool,
2285 pub efi_diagnostics_log_level: EfiDiagnosticsLogLevel,
2287 pub efi_diagnostics_rate_limit: Option<u32>,
2290}
2291
2292impl Default for UefiConfig {
2293 fn default() -> Self {
2294 Self {
2295 secure_boot_enabled: false,
2296 secure_boot_template: None,
2297 disable_frontpage: true,
2298 default_boot_always_attempt: false,
2299 enable_vpci_boot: false,
2300 force_dma_bounce: false,
2301 efi_diagnostics_log_level: EfiDiagnosticsLogLevel::Default,
2302 efi_diagnostics_rate_limit: None,
2303 }
2304 }
2305}
2306
2307#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2312pub enum EfiDiagnosticsLogLevel {
2313 #[default]
2315 Default,
2316 Info,
2318 Full,
2320}
2321
2322#[derive(Debug, Clone)]
2324pub enum OpenvmmLogConfig {
2325 TestDefault,
2329 BuiltInDefault,
2332 Custom(BTreeMap<String, String>),
2342}
2343
2344#[derive(Debug)]
2346pub struct OpenHclConfig {
2347 pub vmbus_redirect: bool,
2349 pub custom_command_line: Option<String>,
2353 pub log_levels: OpenvmmLogConfig,
2357 pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
2360 pub vtl2_settings: Option<Vtl2Settings>,
2362}
2363
2364impl OpenHclConfig {
2365 pub fn command_line(&self) -> String {
2368 let mut cmdline = self.custom_command_line.clone();
2369
2370 append_cmdline(&mut cmdline, "OPENHCL_MANA_KEEP_ALIVE=host,privatepool");
2372
2373 match &self.log_levels {
2374 OpenvmmLogConfig::TestDefault => {
2375 let default_log_levels = {
2376 let openhcl_tracing = if let Ok(x) =
2378 std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG"))
2379 {
2380 format!("OPENVMM_LOG={x}")
2381 } else {
2382 "OPENVMM_LOG=debug".to_owned()
2383 };
2384 let openhcl_show_spans = if let Ok(x) = std::env::var("OPENVMM_SHOW_SPANS") {
2385 format!("OPENVMM_SHOW_SPANS={x}")
2386 } else {
2387 "OPENVMM_SHOW_SPANS=true".to_owned()
2388 };
2389 format!("{openhcl_tracing} {openhcl_show_spans}")
2390 };
2391 append_cmdline(&mut cmdline, &default_log_levels);
2392 }
2393 OpenvmmLogConfig::BuiltInDefault => {
2394 }
2396 OpenvmmLogConfig::Custom(levels) => {
2397 levels.iter().for_each(|(key, value)| {
2398 append_cmdline(&mut cmdline, format!("{key}={value}"));
2399 });
2400 }
2401 }
2402
2403 cmdline.unwrap_or_default()
2404 }
2405}
2406
2407impl Default for OpenHclConfig {
2408 fn default() -> Self {
2409 Self {
2410 vmbus_redirect: false,
2411 custom_command_line: None,
2412 log_levels: OpenvmmLogConfig::TestDefault,
2413 vtl2_base_address_type: None,
2414 vtl2_settings: None,
2415 }
2416 }
2417}
2418
2419#[derive(Debug)]
2421pub struct TpmConfig {
2422 pub no_persistent_secrets: bool,
2424}
2425
2426impl Default for TpmConfig {
2427 fn default() -> Self {
2428 Self {
2429 no_persistent_secrets: true,
2430 }
2431 }
2432}
2433
2434#[derive(Debug)]
2438pub enum Firmware {
2439 LinuxDirect {
2441 kernel: ResolvedArtifact,
2443 initrd: ResolvedArtifact,
2445 },
2446 OpenhclLinuxDirect {
2448 igvm_path: ResolvedArtifact,
2450 openhcl_config: OpenHclConfig,
2452 },
2453 Pcat {
2455 guest: PcatGuest,
2457 bios_firmware: ResolvedOptionalArtifact,
2459 svga_firmware: ResolvedOptionalArtifact,
2461 ide_controllers: [[Option<Drive>; 2]; 2],
2463 },
2464 OpenhclPcat {
2466 guest: PcatGuest,
2468 igvm_path: ResolvedArtifact,
2470 bios_firmware: ResolvedOptionalArtifact,
2472 svga_firmware: ResolvedOptionalArtifact,
2474 openhcl_config: OpenHclConfig,
2476 },
2477 Uefi {
2479 guest: UefiGuest,
2481 uefi_firmware: ResolvedArtifact,
2483 uefi_config: UefiConfig,
2485 },
2486 OpenhclUefi {
2488 guest: UefiGuest,
2490 isolation: Option<IsolationType>,
2492 igvm_path: ResolvedArtifact,
2494 uefi_config: UefiConfig,
2496 openhcl_config: OpenHclConfig,
2498 },
2499}
2500
2501#[derive(Debug, Copy, Clone, PartialEq, Eq)]
2503pub enum BootDeviceType {
2504 None,
2506 Ide,
2508 IdeViaScsi,
2510 IdeViaNvme,
2512 Scsi,
2514 ScsiViaScsi,
2516 ScsiViaNvme,
2518 Nvme,
2520 NvmeViaScsi,
2522 NvmeViaNvme,
2524 PcieNvme,
2526}
2527
2528impl BootDeviceType {
2529 fn requires_vtl2(&self) -> bool {
2530 match self {
2531 BootDeviceType::None
2532 | BootDeviceType::Ide
2533 | BootDeviceType::Scsi
2534 | BootDeviceType::Nvme
2535 | BootDeviceType::PcieNvme => false,
2536 BootDeviceType::IdeViaScsi
2537 | BootDeviceType::IdeViaNvme
2538 | BootDeviceType::ScsiViaScsi
2539 | BootDeviceType::ScsiViaNvme
2540 | BootDeviceType::NvmeViaScsi
2541 | BootDeviceType::NvmeViaNvme => true,
2542 }
2543 }
2544
2545 fn requires_vpci_boot(&self) -> bool {
2546 matches!(
2547 self,
2548 BootDeviceType::Nvme | BootDeviceType::NvmeViaScsi | BootDeviceType::NvmeViaNvme
2549 )
2550 }
2551
2552 fn requires_vmbus(&self) -> bool {
2553 match self {
2554 BootDeviceType::None | BootDeviceType::Ide | BootDeviceType::PcieNvme => false,
2555 BootDeviceType::IdeViaScsi
2556 | BootDeviceType::IdeViaNvme
2557 | BootDeviceType::Scsi
2558 | BootDeviceType::ScsiViaScsi
2559 | BootDeviceType::ScsiViaNvme
2560 | BootDeviceType::Nvme
2561 | BootDeviceType::NvmeViaScsi
2562 | BootDeviceType::NvmeViaNvme => true,
2563 }
2564 }
2565}
2566
2567impl Firmware {
2568 pub fn linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2570 use petri_artifacts_vmm_test::artifacts::loadable::*;
2571 match arch {
2572 MachineArch::X86_64 => Firmware::LinuxDirect {
2573 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_X64).erase(),
2574 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
2575 },
2576 MachineArch::Aarch64 => Firmware::LinuxDirect {
2577 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_AARCH64).erase(),
2578 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_AARCH64).erase(),
2579 },
2580 }
2581 }
2582
2583 pub fn linux_direct_bzimage(resolver: &ArtifactResolver<'_>) -> Self {
2588 use petri_artifacts_vmm_test::artifacts::loadable::*;
2589 Firmware::LinuxDirect {
2590 kernel: resolver.require(LINUX_DIRECT_TEST_BZIMAGE_X64).erase(),
2591 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
2592 }
2593 }
2594
2595 pub fn openhcl_linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2597 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2598 match arch {
2599 MachineArch::X86_64 => Firmware::OpenhclLinuxDirect {
2600 igvm_path: resolver.require(LATEST_LINUX_DIRECT_TEST_X64).erase(),
2601 openhcl_config: Default::default(),
2602 },
2603 MachineArch::Aarch64 => todo!("Linux direct not yet supported on aarch64"),
2604 }
2605 }
2606
2607 pub fn pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2609 use petri_artifacts_vmm_test::artifacts::loadable::*;
2610 Firmware::Pcat {
2611 guest,
2612 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2613 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2614 ide_controllers: [[None, None], [None, None]],
2615 }
2616 }
2617
2618 pub fn openhcl_pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2620 use petri_artifacts_vmm_test::artifacts::loadable::*;
2621 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2622 Firmware::OpenhclPcat {
2623 guest,
2624 igvm_path: resolver.require(LATEST_STANDARD_X64).erase(),
2625 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2626 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2627 openhcl_config: OpenHclConfig {
2628 vmbus_redirect: true,
2630 ..Default::default()
2631 },
2632 }
2633 }
2634
2635 pub fn uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch, guest: UefiGuest) -> Self {
2637 use petri_artifacts_vmm_test::artifacts::loadable::*;
2638 let uefi_firmware = match arch {
2639 MachineArch::X86_64 => resolver.require(UEFI_FIRMWARE_X64).erase(),
2640 MachineArch::Aarch64 => resolver.require(UEFI_FIRMWARE_AARCH64).erase(),
2641 };
2642 Firmware::Uefi {
2643 guest,
2644 uefi_firmware,
2645 uefi_config: Default::default(),
2646 }
2647 }
2648
2649 pub fn openhcl_uefi(
2651 resolver: &ArtifactResolver<'_>,
2652 arch: MachineArch,
2653 guest: UefiGuest,
2654 isolation: Option<IsolationType>,
2655 ) -> Self {
2656 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2657 let igvm_path = match arch {
2658 MachineArch::X86_64 if isolation.is_some() => resolver.require(LATEST_CVM_X64).erase(),
2659 MachineArch::X86_64 => resolver.require(LATEST_STANDARD_X64).erase(),
2660 MachineArch::Aarch64 => resolver.require(LATEST_STANDARD_AARCH64).erase(),
2661 };
2662 Firmware::OpenhclUefi {
2663 guest,
2664 isolation,
2665 igvm_path,
2666 uefi_config: Default::default(),
2667 openhcl_config: Default::default(),
2668 }
2669 }
2670
2671 fn is_openhcl(&self) -> bool {
2672 match self {
2673 Firmware::OpenhclLinuxDirect { .. }
2674 | Firmware::OpenhclUefi { .. }
2675 | Firmware::OpenhclPcat { .. } => true,
2676 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => false,
2677 }
2678 }
2679
2680 fn isolation(&self) -> Option<IsolationType> {
2681 match self {
2682 Firmware::OpenhclUefi { isolation, .. } => *isolation,
2683 Firmware::LinuxDirect { .. }
2684 | Firmware::Pcat { .. }
2685 | Firmware::Uefi { .. }
2686 | Firmware::OpenhclLinuxDirect { .. }
2687 | Firmware::OpenhclPcat { .. } => None,
2688 }
2689 }
2690
2691 fn is_linux_direct(&self) -> bool {
2692 match self {
2693 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => true,
2694 Firmware::Pcat { .. }
2695 | Firmware::Uefi { .. }
2696 | Firmware::OpenhclUefi { .. }
2697 | Firmware::OpenhclPcat { .. } => false,
2698 }
2699 }
2700
2701 pub fn linux_direct_initrd(&self) -> Option<&Path> {
2703 match self {
2704 Firmware::LinuxDirect { initrd, .. } => Some(initrd.get()),
2705 _ => None,
2706 }
2707 }
2708
2709 fn is_pcat(&self) -> bool {
2710 match self {
2711 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => true,
2712 Firmware::Uefi { .. }
2713 | Firmware::OpenhclUefi { .. }
2714 | Firmware::LinuxDirect { .. }
2715 | Firmware::OpenhclLinuxDirect { .. } => false,
2716 }
2717 }
2718
2719 fn os_flavor(&self) -> OsFlavor {
2720 match self {
2721 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => OsFlavor::Linux,
2722 Firmware::Uefi {
2723 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2724 ..
2725 }
2726 | Firmware::OpenhclUefi {
2727 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2728 ..
2729 } => OsFlavor::Uefi,
2730 Firmware::Pcat {
2731 guest: PcatGuest::Vhd(cfg),
2732 ..
2733 }
2734 | Firmware::OpenhclPcat {
2735 guest: PcatGuest::Vhd(cfg),
2736 ..
2737 }
2738 | Firmware::Uefi {
2739 guest: UefiGuest::Vhd(cfg),
2740 ..
2741 }
2742 | Firmware::OpenhclUefi {
2743 guest: UefiGuest::Vhd(cfg),
2744 ..
2745 } => cfg.os_flavor,
2746 Firmware::Pcat {
2747 guest: PcatGuest::Iso(cfg),
2748 ..
2749 }
2750 | Firmware::OpenhclPcat {
2751 guest: PcatGuest::Iso(cfg),
2752 ..
2753 } => cfg.os_flavor,
2754 }
2755 }
2756
2757 fn quirks(&self) -> GuestQuirks {
2758 match self {
2759 Firmware::Pcat {
2760 guest: PcatGuest::Vhd(cfg),
2761 ..
2762 }
2763 | Firmware::Uefi {
2764 guest: UefiGuest::Vhd(cfg),
2765 ..
2766 }
2767 | Firmware::OpenhclUefi {
2768 guest: UefiGuest::Vhd(cfg),
2769 ..
2770 } => cfg.quirks.clone(),
2771 Firmware::Pcat {
2772 guest: PcatGuest::Iso(cfg),
2773 ..
2774 } => cfg.quirks.clone(),
2775 _ => Default::default(),
2776 }
2777 }
2778
2779 fn expected_boot_event(&self) -> Option<FirmwareEvent> {
2780 match self {
2781 Firmware::LinuxDirect { .. }
2782 | Firmware::OpenhclLinuxDirect { .. }
2783 | Firmware::Uefi {
2784 guest: UefiGuest::GuestTestUefi(_),
2785 ..
2786 }
2787 | Firmware::OpenhclUefi {
2788 guest: UefiGuest::GuestTestUefi(_),
2789 ..
2790 } => None,
2791 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => {
2792 Some(FirmwareEvent::BootAttempt)
2794 }
2795 Firmware::Uefi {
2796 guest: UefiGuest::None,
2797 ..
2798 }
2799 | Firmware::OpenhclUefi {
2800 guest: UefiGuest::None,
2801 ..
2802 } => Some(FirmwareEvent::NoBootDevice),
2803 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => {
2804 Some(FirmwareEvent::BootSuccess)
2805 }
2806 }
2807 }
2808
2809 fn openhcl_config(&self) -> Option<&OpenHclConfig> {
2810 match self {
2811 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2812 | Firmware::OpenhclUefi { openhcl_config, .. }
2813 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
2814 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2815 }
2816 }
2817
2818 fn openhcl_config_mut(&mut self) -> Option<&mut OpenHclConfig> {
2819 match self {
2820 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2821 | Firmware::OpenhclUefi { openhcl_config, .. }
2822 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
2823 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2824 }
2825 }
2826
2827 #[cfg_attr(not(windows), expect(dead_code))]
2828 fn openhcl_firmware(&self) -> Option<&Path> {
2829 match self {
2830 Firmware::OpenhclLinuxDirect { igvm_path, .. }
2831 | Firmware::OpenhclUefi { igvm_path, .. }
2832 | Firmware::OpenhclPcat { igvm_path, .. } => Some(igvm_path.get()),
2833 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2834 }
2835 }
2836
2837 fn into_runtime_config(
2838 self,
2839 vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
2840 ) -> PetriVmRuntimeConfig {
2841 match self {
2842 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2843 | Firmware::OpenhclUefi { openhcl_config, .. }
2844 | Firmware::OpenhclPcat { openhcl_config, .. } => PetriVmRuntimeConfig {
2845 vtl2_settings: Some(
2846 openhcl_config
2847 .vtl2_settings
2848 .unwrap_or_else(default_vtl2_settings),
2849 ),
2850 ide_controllers: None,
2851 vmbus_storage_controllers,
2852 },
2853 Firmware::Pcat {
2854 ide_controllers, ..
2855 } => PetriVmRuntimeConfig {
2856 vtl2_settings: None,
2857 ide_controllers: Some(ide_controllers),
2858 vmbus_storage_controllers,
2859 },
2860 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } => PetriVmRuntimeConfig {
2861 vtl2_settings: None,
2862 ide_controllers: None,
2863 vmbus_storage_controllers,
2864 },
2865 }
2866 }
2867
2868 fn uefi_config(&self) -> Option<&UefiConfig> {
2869 match self {
2870 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
2871 Some(uefi_config)
2872 }
2873 Firmware::LinuxDirect { .. }
2874 | Firmware::OpenhclLinuxDirect { .. }
2875 | Firmware::Pcat { .. }
2876 | Firmware::OpenhclPcat { .. } => None,
2877 }
2878 }
2879
2880 fn uefi_config_mut(&mut self) -> Option<&mut UefiConfig> {
2881 match self {
2882 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
2883 Some(uefi_config)
2884 }
2885 Firmware::LinuxDirect { .. }
2886 | Firmware::OpenhclLinuxDirect { .. }
2887 | Firmware::Pcat { .. }
2888 | Firmware::OpenhclPcat { .. } => None,
2889 }
2890 }
2891
2892 fn boot_drive(&self) -> Option<Drive> {
2893 match self {
2894 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => None,
2895 Firmware::Pcat { guest, .. } | Firmware::OpenhclPcat { guest, .. } => {
2896 Some((guest.disk_path(), guest.is_dvd()))
2897 }
2898 Firmware::Uefi { guest, .. } | Firmware::OpenhclUefi { guest, .. } => {
2899 guest.disk_path().map(|dp| (dp, false))
2900 }
2901 }
2902 .map(|(disk_path, is_dvd)| Drive::new(Some(Disk::Differencing(disk_path)), is_dvd))
2903 }
2904
2905 fn vtl2_settings(&mut self) -> Option<&mut Vtl2Settings> {
2906 self.openhcl_config_mut()
2907 .map(|c| c.vtl2_settings.get_or_insert_with(default_vtl2_settings))
2908 }
2909
2910 fn ide_controllers(&self) -> Option<&[[Option<Drive>; 2]; 2]> {
2911 match self {
2912 Firmware::Pcat {
2913 ide_controllers, ..
2914 } => Some(ide_controllers),
2915 _ => None,
2916 }
2917 }
2918
2919 fn ide_controllers_mut(&mut self) -> Option<&mut [[Option<Drive>; 2]; 2]> {
2920 match self {
2921 Firmware::Pcat {
2922 ide_controllers, ..
2923 } => Some(ide_controllers),
2924 _ => None,
2925 }
2926 }
2927}
2928
2929#[derive(Debug)]
2932pub enum PcatGuest {
2933 Vhd(BootImageConfig<boot_image_type::Vhd>),
2935 Iso(BootImageConfig<boot_image_type::Iso>),
2937}
2938
2939impl PcatGuest {
2940 fn disk_path(&self) -> DiskPath {
2941 match self {
2942 PcatGuest::Vhd(disk) => disk.disk_path(),
2943 PcatGuest::Iso(disk) => disk.disk_path(),
2944 }
2945 }
2946
2947 fn is_dvd(&self) -> bool {
2948 matches!(self, Self::Iso(_))
2949 }
2950}
2951
2952#[derive(Debug)]
2955pub enum UefiGuest {
2956 Vhd(BootImageConfig<boot_image_type::Vhd>),
2958 GuestTestUefi(ResolvedArtifact),
2960 None,
2962}
2963
2964impl UefiGuest {
2965 pub fn guest_test_uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2967 use petri_artifacts_vmm_test::artifacts::test_vhd::*;
2968 let artifact = match arch {
2969 MachineArch::X86_64 => resolver.require(GUEST_TEST_UEFI_X64).erase(),
2970 MachineArch::Aarch64 => resolver.require(GUEST_TEST_UEFI_AARCH64).erase(),
2971 };
2972 UefiGuest::GuestTestUefi(artifact)
2973 }
2974
2975 fn disk_path(&self) -> Option<DiskPath> {
2976 match self {
2977 UefiGuest::Vhd(vhd) => Some(vhd.disk_path()),
2978 UefiGuest::GuestTestUefi(p) => Some(DiskPath::Local(p.get().to_path_buf())),
2979 UefiGuest::None => None,
2980 }
2981 }
2982}
2983
2984pub mod boot_image_type {
2986 mod private {
2987 pub trait Sealed {}
2988 impl Sealed for super::Vhd {}
2989 impl Sealed for super::Iso {}
2990 }
2991
2992 pub trait BootImageType: private::Sealed {}
2995
2996 #[derive(Debug)]
2998 pub enum Vhd {}
2999
3000 #[derive(Debug)]
3002 pub enum Iso {}
3003
3004 impl BootImageType for Vhd {}
3005 impl BootImageType for Iso {}
3006}
3007
3008#[derive(Debug)]
3010pub struct BootImageConfig<T: boot_image_type::BootImageType> {
3011 artifact: ResolvedArtifactSource,
3013 os_flavor: OsFlavor,
3015 quirks: GuestQuirks,
3019 _type: core::marker::PhantomData<T>,
3021}
3022
3023impl<T: boot_image_type::BootImageType> BootImageConfig<T> {
3024 fn disk_path(&self) -> DiskPath {
3026 match self.artifact.get() {
3027 ArtifactSource::Local(p) => DiskPath::Local(p.clone()),
3028 ArtifactSource::Remote { url } => DiskPath::Remote { url: url.clone() },
3029 }
3030 }
3031}
3032
3033impl BootImageConfig<boot_image_type::Vhd> {
3034 pub fn from_vhd<A>(artifact: ResolvedArtifactSource<A>) -> Self
3036 where
3037 A: petri_artifacts_common::tags::IsTestVhd,
3038 {
3039 BootImageConfig {
3040 artifact: artifact.erase(),
3041 os_flavor: A::OS_FLAVOR,
3042 quirks: A::quirks(),
3043 _type: std::marker::PhantomData,
3044 }
3045 }
3046}
3047
3048impl BootImageConfig<boot_image_type::Iso> {
3049 pub fn from_iso<A>(artifact: ResolvedArtifactSource<A>) -> Self
3051 where
3052 A: petri_artifacts_common::tags::IsTestIso,
3053 {
3054 BootImageConfig {
3055 artifact: artifact.erase(),
3056 os_flavor: A::OS_FLAVOR,
3057 quirks: A::quirks(),
3058 _type: std::marker::PhantomData,
3059 }
3060 }
3061}
3062
3063#[derive(Debug, Clone, Copy)]
3065pub enum IsolationType {
3066 Vbs,
3068 Snp,
3070 Tdx,
3072}
3073
3074#[derive(Debug, Clone, Copy)]
3076pub struct OpenHclServicingFlags {
3077 pub enable_nvme_keepalive: bool,
3080 pub enable_mana_keepalive: bool,
3082 pub override_version_checks: bool,
3084 pub stop_timeout_hint_secs: Option<u16>,
3086}
3087
3088#[derive(Debug, Clone)]
3090pub enum DiskPath {
3091 Local(PathBuf),
3093 Remote {
3095 url: String,
3097 },
3098}
3099
3100impl From<PathBuf> for DiskPath {
3101 fn from(path: PathBuf) -> Self {
3102 DiskPath::Local(path)
3103 }
3104}
3105
3106#[derive(Debug, Clone)]
3108pub enum Disk {
3109 Memory(u64),
3111 Differencing(DiskPath),
3113 Persistent(PathBuf),
3115 Temporary(Arc<TempPath>),
3117}
3118
3119#[derive(Debug, Clone)]
3121pub struct PetriVmgsDisk {
3122 pub disk: Disk,
3124 pub encryption_policy: GuestStateEncryptionPolicy,
3126}
3127
3128impl Default for PetriVmgsDisk {
3129 fn default() -> Self {
3130 PetriVmgsDisk {
3131 disk: Disk::Memory(vmgs_format::VMGS_DEFAULT_CAPACITY),
3132 encryption_policy: GuestStateEncryptionPolicy::None(false),
3134 }
3135 }
3136}
3137
3138#[derive(Debug, Clone)]
3140pub enum PetriVmgsResource {
3141 Disk(PetriVmgsDisk),
3143 ReprovisionOnFailure(PetriVmgsDisk),
3145 Reprovision(PetriVmgsDisk),
3147 Ephemeral,
3149}
3150
3151impl PetriVmgsResource {
3152 pub fn vmgs(&self) -> Option<&PetriVmgsDisk> {
3154 match self {
3155 PetriVmgsResource::Disk(vmgs)
3156 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
3157 | PetriVmgsResource::Reprovision(vmgs) => Some(vmgs),
3158 PetriVmgsResource::Ephemeral => None,
3159 }
3160 }
3161
3162 pub fn disk(&self) -> Option<&Disk> {
3164 self.vmgs().map(|vmgs| &vmgs.disk)
3165 }
3166
3167 pub fn encryption_policy(&self) -> Option<GuestStateEncryptionPolicy> {
3169 self.vmgs().map(|vmgs| vmgs.encryption_policy)
3170 }
3171}
3172
3173#[derive(Debug, Clone, Copy)]
3175pub enum PetriGuestStateLifetime {
3176 Disk,
3179 ReprovisionOnFailure,
3181 Reprovision,
3183 Ephemeral,
3185}
3186
3187#[derive(Debug, Clone, Copy)]
3189pub enum SecureBootTemplate {
3190 MicrosoftWindows,
3192 MicrosoftUefiCertificateAuthority,
3194}
3195
3196#[derive(Default, Debug, Clone)]
3199pub struct VmmQuirks {
3200 pub flaky_boot: Option<Duration>,
3203}
3204
3205fn make_vm_safe_name(name: &str) -> String {
3211 const MAX_VM_NAME_LENGTH: usize = 100;
3212 const HASH_LENGTH: usize = 4;
3213 const MAX_PREFIX_LENGTH: usize = MAX_VM_NAME_LENGTH - HASH_LENGTH;
3214
3215 if name.len() <= MAX_VM_NAME_LENGTH {
3216 name.to_owned()
3217 } else {
3218 let mut hasher = DefaultHasher::new();
3220 name.hash(&mut hasher);
3221 let hash = hasher.finish();
3222
3223 let hash_suffix = format!("{:04x}", hash & 0xFFFF);
3225
3226 let truncated = &name[..MAX_PREFIX_LENGTH];
3228 tracing::debug!(
3229 "VM name too long ({}), truncating '{}' to '{}{}'",
3230 name.len(),
3231 name,
3232 truncated,
3233 hash_suffix
3234 );
3235
3236 format!("{}{}", truncated, hash_suffix)
3237 }
3238}
3239
3240#[derive(Debug, Clone, Copy, Eq, PartialEq)]
3242pub enum PetriHaltReason {
3243 PowerOff,
3245 Reset,
3247 Hibernate,
3249 TripleFault,
3251 Other,
3253}
3254
3255impl PetriHaltReason {
3256 pub fn with_detail(self, detail: String) -> PetriHaltReasonDetail {
3258 PetriHaltReasonDetail {
3259 reason: self,
3260 detail,
3261 }
3262 }
3263}
3264
3265#[derive(Debug, Clone)]
3267pub struct PetriHaltReasonDetail {
3268 pub reason: PetriHaltReason,
3270 pub detail: String,
3272}
3273
3274fn append_cmdline(cmd: &mut Option<String>, add_cmd: impl AsRef<str>) {
3275 if let Some(cmd) = cmd.as_mut() {
3276 cmd.push(' ');
3277 cmd.push_str(add_cmd.as_ref());
3278 } else {
3279 *cmd = Some(add_cmd.as_ref().to_string());
3280 }
3281}
3282
3283async fn save_inspect(
3284 name: &str,
3285 inspect: std::pin::Pin<Box<dyn Future<Output = anyhow::Result<inspect::Node>> + Send>>,
3286 log_source: &PetriLogSource,
3287) {
3288 tracing::info!("Collecting {name} inspect details.");
3289 let node = match inspect.await {
3290 Ok(n) => n,
3291 Err(e) => {
3292 tracing::error!(?e, "Failed to get {name}");
3293 return;
3294 }
3295 };
3296 if let Err(e) = log_source.write_attachment(
3297 &format!("timeout_inspect_{name}.log"),
3298 format!("{node:#}").as_bytes(),
3299 ) {
3300 tracing::error!(?e, "Failed to save {name} inspect log");
3301 return;
3302 }
3303 tracing::info!("{name} inspect task finished.");
3304}
3305
3306pub struct ModifyFn<T>(pub Box<dyn FnOnce(T) -> T + Send>);
3308
3309impl<T> Debug for ModifyFn<T> {
3310 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3311 write!(f, "_")
3312 }
3313}
3314
3315fn default_vtl2_settings() -> Vtl2Settings {
3317 Vtl2Settings {
3318 version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
3319 fixed: None,
3320 dynamic: Some(Default::default()),
3321 namespace_settings: Default::default(),
3322 }
3323}
3324
3325#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3327pub enum Vtl {
3328 Vtl0 = 0,
3330 Vtl1 = 1,
3332 Vtl2 = 2,
3334}
3335
3336#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3338pub enum VmbusStorageType {
3339 Scsi,
3341 Nvme,
3343 VirtioBlk,
3345}
3346
3347#[derive(Debug, Clone)]
3349pub struct Drive {
3350 pub disk: Option<Disk>,
3352 pub is_dvd: bool,
3354}
3355
3356impl Drive {
3357 pub fn new(disk: Option<Disk>, is_dvd: bool) -> Self {
3359 Self { disk, is_dvd }
3360 }
3361}
3362
3363#[derive(Debug, Clone)]
3365pub struct VmbusStorageController {
3366 pub target_vtl: Vtl,
3368 pub controller_type: VmbusStorageType,
3370 pub drives: HashMap<u32, Drive>,
3372}
3373
3374impl VmbusStorageController {
3375 pub fn new(target_vtl: Vtl, controller_type: VmbusStorageType) -> Self {
3377 Self {
3378 target_vtl,
3379 controller_type,
3380 drives: HashMap::new(),
3381 }
3382 }
3383
3384 pub fn set_drive(
3386 &mut self,
3387 lun: Option<u32>,
3388 drive: Drive,
3389 allow_modify_existing: bool,
3390 ) -> u32 {
3391 let lun = lun.unwrap_or_else(|| {
3392 let mut lun = None;
3394 for x in 0..u8::MAX as u32 {
3395 if !self.drives.contains_key(&x) {
3396 lun = Some(x);
3397 break;
3398 }
3399 }
3400 lun.expect("all locations on this controller are in use")
3401 });
3402
3403 if self.drives.insert(lun, drive).is_some() && !allow_modify_existing {
3404 panic!("a disk with lun {lun} already existed on this controller");
3405 }
3406
3407 lun
3408 }
3409}
3410
3411pub(crate) fn petri_disk_cache_dir() -> String {
3413 if let Ok(dir) = std::env::var("PETRI_CACHE_DIR") {
3414 return dir;
3415 }
3416
3417 #[cfg(target_os = "macos")]
3418 {
3419 if let Ok(home) = std::env::var("HOME") {
3420 return format!("{home}/Library/Caches/petri");
3421 }
3422 }
3423
3424 #[cfg(windows)]
3425 {
3426 if let Ok(local) = std::env::var("LOCALAPPDATA") {
3427 return format!("{local}\\petri\\cache");
3428 }
3429 }
3430
3431 if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
3433 return format!("{xdg}/petri");
3434 }
3435 if let Ok(home) = std::env::var("HOME") {
3436 return format!("{home}/.cache/petri");
3437 }
3438
3439 ".cache/petri".to_string()
3440}
3441
3442#[cfg(test)]
3443mod tests {
3444 use super::make_vm_safe_name;
3445 use crate::Drive;
3446 use crate::VmbusStorageController;
3447 use crate::VmbusStorageType;
3448 use crate::Vtl;
3449
3450 #[test]
3451 fn test_short_names_unchanged() {
3452 let short_name = "short_test_name";
3453 assert_eq!(make_vm_safe_name(short_name), short_name);
3454 }
3455
3456 #[test]
3457 fn test_exactly_100_chars_unchanged() {
3458 let name_100 = "a".repeat(100);
3459 assert_eq!(make_vm_safe_name(&name_100), name_100);
3460 }
3461
3462 #[test]
3463 fn test_long_name_truncated() {
3464 let long_name = "multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_openhcl_servicing";
3465 let result = make_vm_safe_name(long_name);
3466
3467 assert_eq!(result.len(), 100);
3469
3470 assert!(result.starts_with("multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_ope"));
3472
3473 let suffix = &result[96..];
3475 assert_eq!(suffix.len(), 4);
3476 assert!(u16::from_str_radix(suffix, 16).is_ok());
3478 }
3479
3480 #[test]
3481 fn test_deterministic_results() {
3482 let long_name = "very_long_test_name_that_exceeds_the_100_character_limit_and_should_be_truncated_consistently_every_time";
3483 let result1 = make_vm_safe_name(long_name);
3484 let result2 = make_vm_safe_name(long_name);
3485
3486 assert_eq!(result1, result2);
3487 assert_eq!(result1.len(), 100);
3488 }
3489
3490 #[test]
3491 fn test_different_names_different_hashes() {
3492 let name1 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_1";
3493 let name2 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_2";
3494
3495 let result1 = make_vm_safe_name(name1);
3496 let result2 = make_vm_safe_name(name2);
3497
3498 assert_eq!(result1.len(), 100);
3500 assert_eq!(result2.len(), 100);
3501
3502 assert_ne!(result1, result2);
3504 assert_ne!(&result1[96..], &result2[96..]);
3505 }
3506
3507 #[test]
3508 fn test_vmbus_storage_controller() {
3509 let mut controller = VmbusStorageController::new(Vtl::Vtl0, VmbusStorageType::Scsi);
3510 assert_eq!(
3511 controller.set_drive(Some(1), Drive::new(None, false), false),
3512 1
3513 );
3514 assert!(controller.drives.contains_key(&1));
3515 assert_eq!(
3516 controller.set_drive(None, Drive::new(None, false), false),
3517 0
3518 );
3519 assert!(controller.drives.contains_key(&0));
3520 assert_eq!(
3521 controller.set_drive(None, Drive::new(None, false), false),
3522 2
3523 );
3524 assert!(controller.drives.contains_key(&2));
3525 assert_eq!(
3526 controller.set_drive(Some(0), Drive::new(None, false), true),
3527 0
3528 );
3529 assert!(controller.drives.contains_key(&0));
3530 }
3531}