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("").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_hardware_sealing_policy(mut self, policy: PetriHardwareSealingPolicy) -> Self {
1540 self.config
1541 .tpm
1542 .as_mut()
1543 .expect("hardware sealing policy requires a TPM")
1544 .hardware_sealing_policy = policy;
1545 self
1546 }
1547
1548 pub fn with_custom_vtl2_settings(
1552 mut self,
1553 f: impl FnOnce(&mut Vtl2Settings) + 'static + Send + Sync,
1554 ) -> Self {
1555 f(self
1556 .config
1557 .firmware
1558 .vtl2_settings()
1559 .expect("Custom VTL 2 settings are only supported with OpenHCL"));
1560 self
1561 }
1562
1563 pub fn add_vtl2_storage_controller(self, controller: StorageController) -> Self {
1565 self.with_custom_vtl2_settings(move |v| {
1566 v.dynamic
1567 .as_mut()
1568 .unwrap()
1569 .storage_controllers
1570 .push(controller)
1571 })
1572 }
1573
1574 pub fn add_vmbus_storage_controller(
1576 mut self,
1577 id: &Guid,
1578 target_vtl: Vtl,
1579 controller_type: VmbusStorageType,
1580 ) -> Self {
1581 if self
1582 .config
1583 .vmbus_storage_controllers
1584 .insert(
1585 *id,
1586 VmbusStorageController::new(target_vtl, controller_type),
1587 )
1588 .is_some()
1589 {
1590 panic!("storage controller {id} already existed");
1591 }
1592 self
1593 }
1594
1595 pub fn add_vmbus_drive(
1597 mut self,
1598 drive: Drive,
1599 controller_id: &Guid,
1600 controller_location: Option<u32>,
1601 ) -> Self {
1602 let controller = self
1603 .config
1604 .vmbus_storage_controllers
1605 .get_mut(controller_id)
1606 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
1607
1608 _ = controller.set_drive(controller_location, drive, false);
1609
1610 self
1611 }
1612
1613 pub fn add_ide_drive(
1615 mut self,
1616 drive: Drive,
1617 controller_number: u32,
1618 controller_location: u8,
1619 ) -> Self {
1620 self.config
1621 .firmware
1622 .ide_controllers_mut()
1623 .expect("Host IDE requires PCAT with no HCL")[controller_number as usize]
1624 [controller_location as usize] = Some(drive);
1625
1626 self
1627 }
1628
1629 pub fn add_physical_nvme_device(mut self, vsid: Guid, device: PhysicalNvmeDevice) -> Self {
1631 if self
1632 .config
1633 .physical_nvme_devices
1634 .insert(vsid, device)
1635 .is_some()
1636 {
1637 panic!("physical NVMe device {vsid} already existed");
1638 }
1639 self
1640 }
1641
1642 pub fn os_flavor(&self) -> OsFlavor {
1644 self.config.firmware.os_flavor()
1645 }
1646
1647 pub fn is_openhcl(&self) -> bool {
1649 self.config.firmware.is_openhcl()
1650 }
1651
1652 pub fn isolation(&self) -> Option<IsolationType> {
1654 self.config.firmware.isolation()
1655 }
1656
1657 pub fn arch(&self) -> MachineArch {
1659 self.config.arch
1660 }
1661
1662 pub fn log_source(&self) -> &PetriLogSource {
1664 &self.resources.log_source
1665 }
1666
1667 pub fn default_servicing_flags(&self) -> OpenHclServicingFlags {
1669 T::default_servicing_flags()
1670 }
1671
1672 pub fn modify_backend(
1674 mut self,
1675 f: impl FnOnce(T::VmmConfig) -> T::VmmConfig + 'static + Send,
1676 ) -> Self {
1677 if self.modify_vmm_config.is_some() {
1678 panic!("only one modify_backend allowed");
1679 }
1680 self.modify_vmm_config = Some(ModifyFn(Box::new(f)));
1681 self
1682 }
1683}
1684
1685impl<T: PetriVmmBackend> PetriVm<T> {
1686 pub async fn teardown(self) -> anyhow::Result<()> {
1688 tracing::info!("Tearing down VM...");
1689 self.runtime.teardown().await
1690 }
1691
1692 pub async fn wait_for_halt(&mut self) -> anyhow::Result<PetriHaltReasonDetail> {
1694 tracing::info!("Waiting for VM to halt...");
1695 let halt_reason = self.runtime.wait_for_halt(false).await?;
1696 tracing::info!("VM halted: {halt_reason:?}. Cancelling watchdogs...");
1697 futures::future::join_all(self.watchdog_tasks.drain(..).map(|t| t.cancel())).await;
1698 Ok(halt_reason)
1699 }
1700
1701 pub async fn wait_for_clean_shutdown(&mut self) -> anyhow::Result<()> {
1703 let halt_reason = self.wait_for_halt().await?;
1704 if halt_reason.reason != PetriHaltReason::PowerOff {
1705 anyhow::bail!("Expected PowerOff, got {halt_reason:?}");
1706 }
1707 tracing::info!("VM was cleanly powered off and torn down.");
1708 Ok(())
1709 }
1710
1711 pub async fn wait_for_teardown(mut self) -> anyhow::Result<PetriHaltReasonDetail> {
1714 let halt_reason = self.wait_for_halt().await?;
1715 self.teardown().await?;
1716 Ok(halt_reason)
1717 }
1718
1719 pub async fn wait_for_clean_teardown(mut self) -> anyhow::Result<()> {
1721 self.wait_for_clean_shutdown().await?;
1722 self.teardown().await
1723 }
1724
1725 pub async fn wait_for_reset_no_agent(&mut self) -> anyhow::Result<()> {
1727 self.wait_for_reset_core().await?;
1728 self.wait_for_expected_boot_event().await?;
1729 Ok(())
1730 }
1731
1732 pub async fn wait_for_reset(&mut self) -> anyhow::Result<PipetteClient> {
1734 self.wait_for_reset_no_agent().await?;
1735 self.wait_for_agent().await
1736 }
1737
1738 async fn wait_for_reset_core(&mut self) -> anyhow::Result<()> {
1739 tracing::info!("Waiting for VM to reset...");
1740 let halt_reason = self.runtime.wait_for_halt(true).await?;
1741 if halt_reason.reason != PetriHaltReason::Reset {
1742 anyhow::bail!("Expected reset, got {halt_reason:?}");
1743 }
1744 tracing::info!("VM reset.");
1745 Ok(())
1746 }
1747
1748 pub async fn inspect_openhcl(
1759 &self,
1760 path: impl Into<String>,
1761 depth: Option<usize>,
1762 timeout: Option<Duration>,
1763 ) -> anyhow::Result<inspect::Node> {
1764 self.openhcl_diag()?
1765 .inspect(path.into().as_str(), depth, timeout)
1766 .await
1767 }
1768
1769 pub async fn inspect_update_openhcl(
1779 &self,
1780 path: impl Into<String>,
1781 value: impl Into<String>,
1782 ) -> anyhow::Result<inspect::Value> {
1783 self.openhcl_diag()?
1784 .inspect_update(path.into(), value.into())
1785 .await
1786 }
1787
1788 pub async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()> {
1790 self.inspect_openhcl("", None, None).await.map(|_| ())
1791 }
1792
1793 pub async fn inspect_vmm(&self, path: &str) -> anyhow::Result<inspect::Node> {
1804 use anyhow::Context;
1805
1806 let inspector = self
1807 .runtime
1808 .inspector()
1809 .context("this VMM backend does not support inspect")?;
1810 inspector.inspect(path).await
1811 }
1812
1813 pub async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()> {
1819 self.openhcl_diag()?.wait_for_vtl2().await
1820 }
1821
1822 pub async fn kmsg(&self) -> anyhow::Result<diag_client::kmsg_stream::KmsgStream> {
1824 self.openhcl_diag()?.kmsg().await
1825 }
1826
1827 pub async fn openhcl_core_dump(&self, name: &str, path: &Path) -> anyhow::Result<()> {
1830 self.openhcl_diag()?.core_dump(name, path).await
1831 }
1832
1833 pub async fn openhcl_crash(&self, name: &str) -> anyhow::Result<()> {
1835 self.openhcl_diag()?.crash(name).await
1836 }
1837
1838 async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient> {
1841 self.runtime.wait_for_enlightened_shutdown_ready().await?;
1851 self.runtime.wait_for_agent(false).await
1852 }
1853
1854 pub async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient> {
1858 self.launch_vtl2_pipette().await?;
1860 self.runtime.wait_for_agent(true).await
1861 }
1862
1863 async fn wait_for_expected_boot_event(&mut self) -> anyhow::Result<()> {
1870 if let Some(expected_event) = self.expected_boot_event {
1871 let event = self.wait_for_boot_event().await?;
1872
1873 anyhow::ensure!(
1874 event == expected_event,
1875 "Did not receive expected boot event"
1876 );
1877 } else {
1878 tracing::warn!("Boot event not emitted for configured firmware or manually ignored.");
1879 }
1880
1881 Ok(())
1882 }
1883
1884 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
1887 tracing::info!("Waiting for boot event...");
1888 let boot_event = loop {
1889 match CancelContext::new()
1890 .with_timeout(self.vmm_quirks.flaky_boot.unwrap_or(Duration::MAX))
1891 .until_cancelled(self.runtime.wait_for_boot_event())
1892 .await
1893 {
1894 Ok(res) => break res?,
1895 Err(_) => {
1896 tracing::error!("Did not get boot event in required time, resetting...");
1897 if let Some(inspector) = self.runtime.inspector() {
1898 save_inspect(
1899 "vmm",
1900 Box::pin(async move { inspector.inspect("").await }),
1901 &self.resources.log_source,
1902 )
1903 .await;
1904 }
1905
1906 self.runtime.reset().await?;
1907 continue;
1908 }
1909 }
1910 };
1911 tracing::info!("Got boot event: {boot_event:?}");
1912 Ok(boot_event)
1913 }
1914
1915 pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
1918 tracing::info!("Waiting for enlightened shutdown to be ready");
1919 self.runtime.wait_for_enlightened_shutdown_ready().await?;
1920
1921 let mut wait_time = Duration::from_secs(10);
1927
1928 if let Some(duration) = self.guest_quirks.hyperv_shutdown_ic_sleep {
1930 wait_time += duration;
1931 }
1932
1933 tracing::info!(
1934 "Shutdown IC reported ready, waiting for an extra {}s",
1935 wait_time.as_secs()
1936 );
1937 PolledTimer::new(&self.resources.driver)
1938 .sleep(wait_time)
1939 .await;
1940
1941 tracing::info!("Sending enlightened shutdown command");
1942 self.runtime.send_enlightened_shutdown(kind).await
1943 }
1944
1945 pub async fn restart_openhcl(
1948 &mut self,
1949 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1950 flags: OpenHclServicingFlags,
1951 ) -> anyhow::Result<()> {
1952 self.runtime
1953 .restart_openhcl(&new_openhcl.erase(), flags)
1954 .await
1955 }
1956
1957 pub async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()> {
1960 self.runtime.update_command_line(command_line).await
1961 }
1962
1963 pub async fn add_pcie_device(
1965 &mut self,
1966 port_name: String,
1967 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
1968 ) -> anyhow::Result<()> {
1969 self.runtime.add_pcie_device(port_name, resource).await
1970 }
1971
1972 pub async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
1974 self.runtime.remove_pcie_device(port_name).await
1975 }
1976
1977 pub async fn save_openhcl(
1980 &mut self,
1981 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1982 flags: OpenHclServicingFlags,
1983 ) -> anyhow::Result<()> {
1984 self.runtime.save_openhcl(&new_openhcl.erase(), flags).await
1985 }
1986
1987 pub async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
1990 self.runtime.restore_openhcl().await
1991 }
1992
1993 pub fn arch(&self) -> MachineArch {
1995 self.arch
1996 }
1997
1998 pub fn backend(&mut self) -> &mut T::VmRuntime {
2000 &mut self.runtime
2001 }
2002
2003 async fn launch_vtl2_pipette(&self) -> anyhow::Result<()> {
2004 tracing::debug!("Launching VTL 2 pipette...");
2005
2006 let res = self
2008 .openhcl_diag()?
2009 .run_vtl2_command("sh", &["-c", "mkdir /cidata && mount LABEL=cidata /cidata"])
2010 .await?;
2011
2012 if !res.exit_status.success() {
2013 anyhow::bail!("Failed to mount VTL 2 pipette drive: {:?}", res);
2014 }
2015
2016 let res = self
2017 .openhcl_diag()?
2018 .run_detached_vtl2_command("sh", &["-c", "/cidata/pipette 2>&1 | logger &"])
2019 .await?;
2020
2021 if !res.success() {
2022 anyhow::bail!("Failed to spawn VTL 2 pipette: {:?}", res);
2023 }
2024
2025 Ok(())
2026 }
2027
2028 fn openhcl_diag(&self) -> anyhow::Result<&OpenHclDiagHandler> {
2029 if let Some(ohd) = self.openhcl_diag_handler.as_ref() {
2030 Ok(ohd)
2031 } else {
2032 anyhow::bail!("VM is not configured with OpenHCL")
2033 }
2034 }
2035
2036 pub async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2038 self.runtime.get_guest_state_file().await
2039 }
2040
2041 pub async fn modify_vtl2_settings(
2043 &mut self,
2044 f: impl FnOnce(&mut Vtl2Settings),
2045 ) -> anyhow::Result<()> {
2046 if self.openhcl_diag_handler.is_none() {
2047 panic!("Custom VTL 2 settings are only supported with OpenHCL");
2048 }
2049 f(self
2050 .config
2051 .vtl2_settings
2052 .get_or_insert_with(default_vtl2_settings));
2053 self.runtime
2054 .set_vtl2_settings(self.config.vtl2_settings.as_ref().unwrap())
2055 .await
2056 }
2057
2058 pub fn get_vmbus_storage_controllers(&self) -> &HashMap<Guid, VmbusStorageController> {
2060 &self.config.vmbus_storage_controllers
2061 }
2062
2063 pub async fn set_vmbus_drive(
2065 &mut self,
2066 drive: Drive,
2067 controller_id: &Guid,
2068 controller_location: Option<u32>,
2069 ) -> anyhow::Result<()> {
2070 let controller = self
2071 .config
2072 .vmbus_storage_controllers
2073 .get_mut(controller_id)
2074 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
2075
2076 let controller_location = controller.set_drive(controller_location, drive, true);
2077 let disk = controller.drives.get(&controller_location).unwrap();
2078
2079 self.runtime
2080 .set_vmbus_drive(disk, controller_id, controller_location)
2081 .await?;
2082
2083 Ok(())
2084 }
2085}
2086
2087#[async_trait]
2089pub trait PetriVmRuntime: Send + Sync + 'static {
2090 type VmInspector: PetriVmInspector;
2092 type VmFramebufferAccess: PetriVmFramebufferAccess;
2094
2095 async fn teardown(self) -> anyhow::Result<()>;
2097 async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReasonDetail>;
2100 async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>;
2102 fn openhcl_diag(&self) -> Option<OpenHclDiagHandler>;
2104 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent>;
2107 async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()>;
2110 async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
2112 async fn restart_openhcl(
2115 &mut self,
2116 new_openhcl: &ResolvedArtifact,
2117 flags: OpenHclServicingFlags,
2118 ) -> anyhow::Result<()>;
2119 async fn save_openhcl(
2123 &mut self,
2124 new_openhcl: &ResolvedArtifact,
2125 flags: OpenHclServicingFlags,
2126 ) -> anyhow::Result<()>;
2127 async fn restore_openhcl(&mut self) -> anyhow::Result<()>;
2130 async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()>;
2133 fn inspector(&self) -> Option<Self::VmInspector> {
2135 None
2136 }
2137 fn take_framebuffer_access(&mut self) -> Option<Self::VmFramebufferAccess> {
2140 None
2141 }
2142 async fn reset(&mut self) -> anyhow::Result<()>;
2144 async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2146 Ok(None)
2147 }
2148 async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()>;
2150 async fn set_vmbus_drive(
2152 &mut self,
2153 disk: &Drive,
2154 controller_id: &Guid,
2155 controller_location: u32,
2156 ) -> anyhow::Result<()>;
2157 async fn add_pcie_device(
2159 &mut self,
2160 port_name: String,
2161 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
2162 ) -> anyhow::Result<()> {
2163 let _ = (port_name, resource);
2164 anyhow::bail!("PCIe hotplug not supported by this backend")
2165 }
2166 async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
2168 let _ = port_name;
2169 anyhow::bail!("PCIe hotplug not supported by this backend")
2170 }
2171}
2172
2173#[async_trait]
2175pub trait PetriVmInspector: Send + Sync + 'static {
2176 async fn inspect(&self, path: &str) -> anyhow::Result<inspect::Node>;
2179}
2180
2181pub struct NoPetriVmInspector;
2183#[async_trait]
2184impl PetriVmInspector for NoPetriVmInspector {
2185 async fn inspect(&self, _path: &str) -> anyhow::Result<inspect::Node> {
2186 unreachable!()
2187 }
2188}
2189
2190pub struct VmScreenshotMeta {
2192 pub color: image::ExtendedColorType,
2194 pub width: u16,
2196 pub height: u16,
2198}
2199
2200#[async_trait]
2202pub trait PetriVmFramebufferAccess: Send + 'static {
2203 async fn screenshot(&mut self, image: &mut Vec<u8>)
2206 -> anyhow::Result<Option<VmScreenshotMeta>>;
2207}
2208
2209pub struct NoPetriVmFramebufferAccess;
2211#[async_trait]
2212impl PetriVmFramebufferAccess for NoPetriVmFramebufferAccess {
2213 async fn screenshot(
2214 &mut self,
2215 _image: &mut Vec<u8>,
2216 ) -> anyhow::Result<Option<VmScreenshotMeta>> {
2217 unreachable!()
2218 }
2219}
2220
2221#[derive(Debug)]
2223pub struct ProcessorTopology {
2224 pub vp_count: u32,
2226 pub enable_smt: Option<bool>,
2228 pub vps_per_socket: Option<u32>,
2230 pub apic_mode: Option<ApicMode>,
2232}
2233
2234impl Default for ProcessorTopology {
2235 fn default() -> Self {
2236 Self {
2237 vp_count: 2,
2238 enable_smt: None,
2239 vps_per_socket: None,
2240 apic_mode: None,
2241 }
2242 }
2243}
2244
2245impl ProcessorTopology {
2246 pub fn heavy() -> Self {
2248 Self {
2249 vp_count: 16,
2250 vps_per_socket: Some(8),
2251 ..Default::default()
2252 }
2253 }
2254
2255 pub fn very_heavy() -> Self {
2257 Self {
2258 vp_count: 32,
2259 vps_per_socket: Some(16),
2260 ..Default::default()
2261 }
2262 }
2263}
2264
2265#[derive(Debug, Clone, Copy)]
2267pub enum ApicMode {
2268 Xapic,
2270 X2apicSupported,
2272 X2apicEnabled,
2274}
2275
2276#[derive(Debug)]
2278pub struct MemoryConfig {
2279 pub startup_bytes: u64,
2282 pub dynamic_memory_range: Option<(u64, u64)>,
2286 pub numa_mem_sizes: Option<Vec<u64>>,
2289}
2290
2291impl Default for MemoryConfig {
2292 fn default() -> Self {
2293 Self {
2294 startup_bytes: 4 * 1024 * 1024 * 1024, dynamic_memory_range: None,
2296 numa_mem_sizes: None,
2297 }
2298 }
2299}
2300
2301#[derive(Debug)]
2303pub struct UefiConfig {
2304 pub secure_boot_enabled: bool,
2306 pub secure_boot_template: Option<SecureBootTemplate>,
2308 pub disable_frontpage: bool,
2310 pub default_boot_always_attempt: bool,
2312 pub enable_vpci_boot: bool,
2314 pub force_dma_bounce: bool,
2316 pub efi_diagnostics_log_level: EfiDiagnosticsLogLevel,
2318 pub efi_diagnostics_rate_limit: Option<u32>,
2321}
2322
2323impl Default for UefiConfig {
2324 fn default() -> Self {
2325 Self {
2326 secure_boot_enabled: false,
2327 secure_boot_template: None,
2328 disable_frontpage: true,
2329 default_boot_always_attempt: false,
2330 enable_vpci_boot: false,
2331 force_dma_bounce: false,
2332 efi_diagnostics_log_level: EfiDiagnosticsLogLevel::Default,
2333 efi_diagnostics_rate_limit: None,
2334 }
2335 }
2336}
2337
2338#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2343pub enum EfiDiagnosticsLogLevel {
2344 #[default]
2346 Default,
2347 Info,
2349 Full,
2351}
2352
2353#[derive(Debug, Clone)]
2355pub enum OpenvmmLogConfig {
2356 TestDefault,
2360 BuiltInDefault,
2363 Custom(BTreeMap<String, String>),
2373}
2374
2375#[derive(Debug)]
2377pub struct OpenHclConfig {
2378 pub vmbus_redirect: bool,
2380 pub custom_command_line: Option<String>,
2384 pub log_levels: OpenvmmLogConfig,
2388 pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
2391 pub vtl2_settings: Option<Vtl2Settings>,
2393}
2394
2395impl OpenHclConfig {
2396 pub fn command_line(&self) -> String {
2399 let mut cmdline = self.custom_command_line.clone();
2400
2401 append_cmdline(&mut cmdline, "OPENHCL_MANA_KEEP_ALIVE=host,privatepool");
2403
2404 match &self.log_levels {
2405 OpenvmmLogConfig::TestDefault => {
2406 let default_log_levels = {
2407 let openhcl_tracing = if let Ok(x) =
2409 std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG"))
2410 {
2411 format!("OPENVMM_LOG={x}")
2412 } else {
2413 "OPENVMM_LOG=debug".to_owned()
2414 };
2415 let openhcl_show_spans = if let Ok(x) = std::env::var("OPENVMM_SHOW_SPANS") {
2416 format!("OPENVMM_SHOW_SPANS={x}")
2417 } else {
2418 "OPENVMM_SHOW_SPANS=true".to_owned()
2419 };
2420 format!("{openhcl_tracing} {openhcl_show_spans}")
2421 };
2422 append_cmdline(&mut cmdline, &default_log_levels);
2423 }
2424 OpenvmmLogConfig::BuiltInDefault => {
2425 }
2427 OpenvmmLogConfig::Custom(levels) => {
2428 levels.iter().for_each(|(key, value)| {
2429 append_cmdline(&mut cmdline, format!("{key}={value}"));
2430 });
2431 }
2432 }
2433
2434 cmdline.unwrap_or_default()
2435 }
2436}
2437
2438impl Default for OpenHclConfig {
2439 fn default() -> Self {
2440 Self {
2441 vmbus_redirect: false,
2442 custom_command_line: None,
2443 log_levels: OpenvmmLogConfig::TestDefault,
2444 vtl2_base_address_type: None,
2445 vtl2_settings: None,
2446 }
2447 }
2448}
2449
2450#[derive(Debug)]
2452pub struct TpmConfig {
2453 pub no_persistent_secrets: bool,
2455 pub hardware_sealing_policy: PetriHardwareSealingPolicy,
2457}
2458
2459impl Default for TpmConfig {
2460 fn default() -> Self {
2461 Self {
2462 no_persistent_secrets: true,
2463 hardware_sealing_policy: PetriHardwareSealingPolicy::Default,
2464 }
2465 }
2466}
2467
2468#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2473pub enum PetriHardwareSealingPolicy {
2474 #[default]
2476 Default,
2477 HashPolicy,
2479 SignerPolicy,
2481}
2482
2483#[derive(Debug)]
2487pub enum Firmware {
2488 LinuxDirect {
2490 kernel: ResolvedArtifact,
2492 initrd: ResolvedArtifact,
2494 },
2495 OpenhclLinuxDirect {
2497 igvm_path: ResolvedArtifact,
2499 openhcl_config: OpenHclConfig,
2501 },
2502 Pcat {
2504 guest: PcatGuest,
2506 bios_firmware: ResolvedOptionalArtifact,
2508 svga_firmware: ResolvedOptionalArtifact,
2510 ide_controllers: [[Option<Drive>; 2]; 2],
2512 },
2513 OpenhclPcat {
2515 guest: PcatGuest,
2517 igvm_path: ResolvedArtifact,
2519 bios_firmware: ResolvedOptionalArtifact,
2521 svga_firmware: ResolvedOptionalArtifact,
2523 openhcl_config: OpenHclConfig,
2525 },
2526 Uefi {
2528 guest: UefiGuest,
2530 uefi_firmware: ResolvedArtifact,
2532 uefi_config: UefiConfig,
2534 },
2535 OpenhclUefi {
2537 guest: UefiGuest,
2539 isolation: Option<IsolationType>,
2541 igvm_path: ResolvedArtifact,
2543 uefi_config: UefiConfig,
2545 openhcl_config: OpenHclConfig,
2547 },
2548}
2549
2550#[derive(Debug, Copy, Clone, PartialEq, Eq)]
2552pub enum BootDeviceType {
2553 None,
2555 Ide,
2557 IdeViaScsi,
2559 IdeViaNvme,
2561 Scsi,
2563 ScsiViaScsi,
2565 ScsiViaNvme,
2567 Nvme,
2569 NvmeViaScsi,
2571 NvmeViaNvme,
2573 PcieNvme,
2575}
2576
2577impl BootDeviceType {
2578 fn requires_vtl2(&self) -> bool {
2579 match self {
2580 BootDeviceType::None
2581 | BootDeviceType::Ide
2582 | BootDeviceType::Scsi
2583 | BootDeviceType::Nvme
2584 | BootDeviceType::PcieNvme => false,
2585 BootDeviceType::IdeViaScsi
2586 | BootDeviceType::IdeViaNvme
2587 | BootDeviceType::ScsiViaScsi
2588 | BootDeviceType::ScsiViaNvme
2589 | BootDeviceType::NvmeViaScsi
2590 | BootDeviceType::NvmeViaNvme => true,
2591 }
2592 }
2593
2594 fn requires_vpci_boot(&self) -> bool {
2595 matches!(
2596 self,
2597 BootDeviceType::Nvme | BootDeviceType::NvmeViaScsi | BootDeviceType::NvmeViaNvme
2598 )
2599 }
2600
2601 fn requires_vmbus(&self) -> bool {
2602 match self {
2603 BootDeviceType::None | BootDeviceType::Ide | BootDeviceType::PcieNvme => false,
2604 BootDeviceType::IdeViaScsi
2605 | BootDeviceType::IdeViaNvme
2606 | BootDeviceType::Scsi
2607 | BootDeviceType::ScsiViaScsi
2608 | BootDeviceType::ScsiViaNvme
2609 | BootDeviceType::Nvme
2610 | BootDeviceType::NvmeViaScsi
2611 | BootDeviceType::NvmeViaNvme => true,
2612 }
2613 }
2614}
2615
2616impl Firmware {
2617 pub fn linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2619 use petri_artifacts_vmm_test::artifacts::loadable::*;
2620 match arch {
2621 MachineArch::X86_64 => Firmware::LinuxDirect {
2622 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_X64).erase(),
2623 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
2624 },
2625 MachineArch::Aarch64 => Firmware::LinuxDirect {
2626 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_AARCH64).erase(),
2627 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_AARCH64).erase(),
2628 },
2629 }
2630 }
2631
2632 pub fn linux_direct_bzimage(resolver: &ArtifactResolver<'_>) -> Self {
2637 use petri_artifacts_vmm_test::artifacts::loadable::*;
2638 Firmware::LinuxDirect {
2639 kernel: resolver.require(LINUX_DIRECT_TEST_BZIMAGE_X64).erase(),
2640 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
2641 }
2642 }
2643
2644 pub fn openhcl_linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2646 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2647 match arch {
2648 MachineArch::X86_64 => Firmware::OpenhclLinuxDirect {
2649 igvm_path: resolver.require(LATEST_LINUX_DIRECT_TEST_X64).erase(),
2650 openhcl_config: Default::default(),
2651 },
2652 MachineArch::Aarch64 => todo!("Linux direct not yet supported on aarch64"),
2653 }
2654 }
2655
2656 pub fn pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2658 use petri_artifacts_vmm_test::artifacts::loadable::*;
2659 Firmware::Pcat {
2660 guest,
2661 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2662 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2663 ide_controllers: [[None, None], [None, None]],
2664 }
2665 }
2666
2667 pub fn openhcl_pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2669 use petri_artifacts_vmm_test::artifacts::loadable::*;
2670 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2671 Firmware::OpenhclPcat {
2672 guest,
2673 igvm_path: resolver.require(LATEST_STANDARD_X64).erase(),
2674 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2675 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2676 openhcl_config: OpenHclConfig {
2677 vmbus_redirect: true,
2679 ..Default::default()
2680 },
2681 }
2682 }
2683
2684 pub fn uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch, guest: UefiGuest) -> Self {
2686 use petri_artifacts_vmm_test::artifacts::loadable::*;
2687 let uefi_firmware = match arch {
2688 MachineArch::X86_64 => resolver.require(UEFI_FIRMWARE_X64).erase(),
2689 MachineArch::Aarch64 => resolver.require(UEFI_FIRMWARE_AARCH64).erase(),
2690 };
2691 Firmware::Uefi {
2692 guest,
2693 uefi_firmware,
2694 uefi_config: Default::default(),
2695 }
2696 }
2697
2698 pub fn openhcl_uefi(
2700 resolver: &ArtifactResolver<'_>,
2701 arch: MachineArch,
2702 guest: UefiGuest,
2703 isolation: Option<IsolationType>,
2704 ) -> Self {
2705 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2706 let igvm_path = match arch {
2707 MachineArch::X86_64 if isolation.is_some() => resolver.require(LATEST_CVM_X64).erase(),
2708 MachineArch::X86_64 => resolver.require(LATEST_STANDARD_X64).erase(),
2709 MachineArch::Aarch64 => resolver.require(LATEST_STANDARD_AARCH64).erase(),
2710 };
2711 Firmware::OpenhclUefi {
2712 guest,
2713 isolation,
2714 igvm_path,
2715 uefi_config: Default::default(),
2716 openhcl_config: Default::default(),
2717 }
2718 }
2719
2720 fn is_openhcl(&self) -> bool {
2721 match self {
2722 Firmware::OpenhclLinuxDirect { .. }
2723 | Firmware::OpenhclUefi { .. }
2724 | Firmware::OpenhclPcat { .. } => true,
2725 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => false,
2726 }
2727 }
2728
2729 fn isolation(&self) -> Option<IsolationType> {
2730 match self {
2731 Firmware::OpenhclUefi { isolation, .. } => *isolation,
2732 Firmware::LinuxDirect { .. }
2733 | Firmware::Pcat { .. }
2734 | Firmware::Uefi { .. }
2735 | Firmware::OpenhclLinuxDirect { .. }
2736 | Firmware::OpenhclPcat { .. } => None,
2737 }
2738 }
2739
2740 fn is_linux_direct(&self) -> bool {
2741 match self {
2742 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => true,
2743 Firmware::Pcat { .. }
2744 | Firmware::Uefi { .. }
2745 | Firmware::OpenhclUefi { .. }
2746 | Firmware::OpenhclPcat { .. } => false,
2747 }
2748 }
2749
2750 pub fn linux_direct_initrd(&self) -> Option<&Path> {
2752 match self {
2753 Firmware::LinuxDirect { initrd, .. } => Some(initrd.get()),
2754 _ => None,
2755 }
2756 }
2757
2758 fn is_pcat(&self) -> bool {
2759 match self {
2760 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => true,
2761 Firmware::Uefi { .. }
2762 | Firmware::OpenhclUefi { .. }
2763 | Firmware::LinuxDirect { .. }
2764 | Firmware::OpenhclLinuxDirect { .. } => false,
2765 }
2766 }
2767
2768 fn os_flavor(&self) -> OsFlavor {
2769 match self {
2770 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => OsFlavor::Linux,
2771 Firmware::Uefi {
2772 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2773 ..
2774 }
2775 | Firmware::OpenhclUefi {
2776 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2777 ..
2778 } => OsFlavor::Uefi,
2779 Firmware::Pcat {
2780 guest: PcatGuest::Vhd(cfg),
2781 ..
2782 }
2783 | Firmware::OpenhclPcat {
2784 guest: PcatGuest::Vhd(cfg),
2785 ..
2786 }
2787 | Firmware::Uefi {
2788 guest: UefiGuest::Vhd(cfg),
2789 ..
2790 }
2791 | Firmware::OpenhclUefi {
2792 guest: UefiGuest::Vhd(cfg),
2793 ..
2794 } => cfg.os_flavor,
2795 Firmware::Pcat {
2796 guest: PcatGuest::Iso(cfg),
2797 ..
2798 }
2799 | Firmware::OpenhclPcat {
2800 guest: PcatGuest::Iso(cfg),
2801 ..
2802 } => cfg.os_flavor,
2803 }
2804 }
2805
2806 fn quirks(&self) -> GuestQuirks {
2807 match self {
2808 Firmware::Pcat {
2809 guest: PcatGuest::Vhd(cfg),
2810 ..
2811 }
2812 | Firmware::Uefi {
2813 guest: UefiGuest::Vhd(cfg),
2814 ..
2815 }
2816 | Firmware::OpenhclUefi {
2817 guest: UefiGuest::Vhd(cfg),
2818 ..
2819 } => cfg.quirks.clone(),
2820 Firmware::Pcat {
2821 guest: PcatGuest::Iso(cfg),
2822 ..
2823 } => cfg.quirks.clone(),
2824 _ => Default::default(),
2825 }
2826 }
2827
2828 fn expected_boot_event(&self) -> Option<FirmwareEvent> {
2829 match self {
2830 Firmware::LinuxDirect { .. }
2831 | Firmware::OpenhclLinuxDirect { .. }
2832 | Firmware::Uefi {
2833 guest: UefiGuest::GuestTestUefi(_),
2834 ..
2835 }
2836 | Firmware::OpenhclUefi {
2837 guest: UefiGuest::GuestTestUefi(_),
2838 ..
2839 } => None,
2840 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => {
2841 Some(FirmwareEvent::BootAttempt)
2843 }
2844 Firmware::Uefi {
2845 guest: UefiGuest::None,
2846 ..
2847 }
2848 | Firmware::OpenhclUefi {
2849 guest: UefiGuest::None,
2850 ..
2851 } => Some(FirmwareEvent::NoBootDevice),
2852 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => {
2853 Some(FirmwareEvent::BootSuccess)
2854 }
2855 }
2856 }
2857
2858 fn openhcl_config(&self) -> Option<&OpenHclConfig> {
2859 match self {
2860 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2861 | Firmware::OpenhclUefi { openhcl_config, .. }
2862 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
2863 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2864 }
2865 }
2866
2867 fn openhcl_config_mut(&mut self) -> Option<&mut OpenHclConfig> {
2868 match self {
2869 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2870 | Firmware::OpenhclUefi { openhcl_config, .. }
2871 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
2872 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2873 }
2874 }
2875
2876 #[cfg_attr(not(windows), expect(dead_code))]
2877 fn openhcl_firmware(&self) -> Option<&Path> {
2878 match self {
2879 Firmware::OpenhclLinuxDirect { igvm_path, .. }
2880 | Firmware::OpenhclUefi { igvm_path, .. }
2881 | Firmware::OpenhclPcat { igvm_path, .. } => Some(igvm_path.get()),
2882 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2883 }
2884 }
2885
2886 fn into_runtime_config(
2887 self,
2888 vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
2889 ) -> PetriVmRuntimeConfig {
2890 match self {
2891 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2892 | Firmware::OpenhclUefi { openhcl_config, .. }
2893 | Firmware::OpenhclPcat { openhcl_config, .. } => PetriVmRuntimeConfig {
2894 vtl2_settings: Some(
2895 openhcl_config
2896 .vtl2_settings
2897 .unwrap_or_else(default_vtl2_settings),
2898 ),
2899 ide_controllers: None,
2900 vmbus_storage_controllers,
2901 },
2902 Firmware::Pcat {
2903 ide_controllers, ..
2904 } => PetriVmRuntimeConfig {
2905 vtl2_settings: None,
2906 ide_controllers: Some(ide_controllers),
2907 vmbus_storage_controllers,
2908 },
2909 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } => PetriVmRuntimeConfig {
2910 vtl2_settings: None,
2911 ide_controllers: None,
2912 vmbus_storage_controllers,
2913 },
2914 }
2915 }
2916
2917 fn uefi_config(&self) -> Option<&UefiConfig> {
2918 match self {
2919 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
2920 Some(uefi_config)
2921 }
2922 Firmware::LinuxDirect { .. }
2923 | Firmware::OpenhclLinuxDirect { .. }
2924 | Firmware::Pcat { .. }
2925 | Firmware::OpenhclPcat { .. } => None,
2926 }
2927 }
2928
2929 fn uefi_config_mut(&mut self) -> Option<&mut UefiConfig> {
2930 match self {
2931 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
2932 Some(uefi_config)
2933 }
2934 Firmware::LinuxDirect { .. }
2935 | Firmware::OpenhclLinuxDirect { .. }
2936 | Firmware::Pcat { .. }
2937 | Firmware::OpenhclPcat { .. } => None,
2938 }
2939 }
2940
2941 fn boot_drive(&self) -> Option<Drive> {
2942 match self {
2943 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => None,
2944 Firmware::Pcat { guest, .. } | Firmware::OpenhclPcat { guest, .. } => {
2945 Some((guest.disk_path(), guest.is_dvd()))
2946 }
2947 Firmware::Uefi { guest, .. } | Firmware::OpenhclUefi { guest, .. } => {
2948 guest.disk_path().map(|dp| (dp, false))
2949 }
2950 }
2951 .map(|(disk_path, is_dvd)| Drive::new(Some(Disk::Differencing(disk_path)), is_dvd))
2952 }
2953
2954 fn vtl2_settings(&mut self) -> Option<&mut Vtl2Settings> {
2955 self.openhcl_config_mut()
2956 .map(|c| c.vtl2_settings.get_or_insert_with(default_vtl2_settings))
2957 }
2958
2959 fn ide_controllers(&self) -> Option<&[[Option<Drive>; 2]; 2]> {
2960 match self {
2961 Firmware::Pcat {
2962 ide_controllers, ..
2963 } => Some(ide_controllers),
2964 _ => None,
2965 }
2966 }
2967
2968 fn ide_controllers_mut(&mut self) -> Option<&mut [[Option<Drive>; 2]; 2]> {
2969 match self {
2970 Firmware::Pcat {
2971 ide_controllers, ..
2972 } => Some(ide_controllers),
2973 _ => None,
2974 }
2975 }
2976}
2977
2978#[derive(Debug)]
2981pub enum PcatGuest {
2982 Vhd(BootImageConfig<boot_image_type::Vhd>),
2984 Iso(BootImageConfig<boot_image_type::Iso>),
2986}
2987
2988impl PcatGuest {
2989 fn disk_path(&self) -> DiskPath {
2990 match self {
2991 PcatGuest::Vhd(disk) => disk.disk_path(),
2992 PcatGuest::Iso(disk) => disk.disk_path(),
2993 }
2994 }
2995
2996 fn is_dvd(&self) -> bool {
2997 matches!(self, Self::Iso(_))
2998 }
2999}
3000
3001#[derive(Debug)]
3004pub enum UefiGuest {
3005 Vhd(BootImageConfig<boot_image_type::Vhd>),
3007 GuestTestUefi(ResolvedArtifact),
3009 None,
3011}
3012
3013impl UefiGuest {
3014 pub fn guest_test_uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
3016 use petri_artifacts_vmm_test::artifacts::test_vhd::*;
3017 let artifact = match arch {
3018 MachineArch::X86_64 => resolver.require(GUEST_TEST_UEFI_X64).erase(),
3019 MachineArch::Aarch64 => resolver.require(GUEST_TEST_UEFI_AARCH64).erase(),
3020 };
3021 UefiGuest::GuestTestUefi(artifact)
3022 }
3023
3024 fn disk_path(&self) -> Option<DiskPath> {
3025 match self {
3026 UefiGuest::Vhd(vhd) => Some(vhd.disk_path()),
3027 UefiGuest::GuestTestUefi(p) => Some(DiskPath::Local(p.get().to_path_buf())),
3028 UefiGuest::None => None,
3029 }
3030 }
3031}
3032
3033pub mod boot_image_type {
3035 mod private {
3036 pub trait Sealed {}
3037 impl Sealed for super::Vhd {}
3038 impl Sealed for super::Iso {}
3039 }
3040
3041 pub trait BootImageType: private::Sealed {}
3044
3045 #[derive(Debug)]
3047 pub enum Vhd {}
3048
3049 #[derive(Debug)]
3051 pub enum Iso {}
3052
3053 impl BootImageType for Vhd {}
3054 impl BootImageType for Iso {}
3055}
3056
3057#[derive(Debug)]
3059pub struct BootImageConfig<T: boot_image_type::BootImageType> {
3060 artifact: ResolvedArtifactSource,
3062 os_flavor: OsFlavor,
3064 quirks: GuestQuirks,
3068 _type: core::marker::PhantomData<T>,
3070}
3071
3072impl<T: boot_image_type::BootImageType> BootImageConfig<T> {
3073 fn disk_path(&self) -> DiskPath {
3075 match self.artifact.get() {
3076 ArtifactSource::Local(p) => DiskPath::Local(p.clone()),
3077 ArtifactSource::Remote { url } => DiskPath::Remote { url: url.clone() },
3078 }
3079 }
3080}
3081
3082impl BootImageConfig<boot_image_type::Vhd> {
3083 pub fn from_vhd<A>(artifact: ResolvedArtifactSource<A>) -> Self
3085 where
3086 A: petri_artifacts_common::tags::IsTestVhd,
3087 {
3088 BootImageConfig {
3089 artifact: artifact.erase(),
3090 os_flavor: A::OS_FLAVOR,
3091 quirks: A::quirks(),
3092 _type: std::marker::PhantomData,
3093 }
3094 }
3095}
3096
3097impl BootImageConfig<boot_image_type::Iso> {
3098 pub fn from_iso<A>(artifact: ResolvedArtifactSource<A>) -> Self
3100 where
3101 A: petri_artifacts_common::tags::IsTestIso,
3102 {
3103 BootImageConfig {
3104 artifact: artifact.erase(),
3105 os_flavor: A::OS_FLAVOR,
3106 quirks: A::quirks(),
3107 _type: std::marker::PhantomData,
3108 }
3109 }
3110}
3111
3112#[derive(Debug, Clone, Copy)]
3114pub enum IsolationType {
3115 Vbs,
3117 Snp,
3119 Tdx,
3121}
3122
3123#[derive(Debug, Clone, Copy)]
3125pub struct OpenHclServicingFlags {
3126 pub enable_nvme_keepalive: bool,
3129 pub enable_mana_keepalive: bool,
3131 pub override_version_checks: bool,
3133 pub stop_timeout_hint_secs: Option<u16>,
3135}
3136
3137#[derive(Debug, Clone)]
3139pub enum DiskPath {
3140 Local(PathBuf),
3142 Remote {
3144 url: String,
3146 },
3147}
3148
3149impl From<PathBuf> for DiskPath {
3150 fn from(path: PathBuf) -> Self {
3151 DiskPath::Local(path)
3152 }
3153}
3154
3155#[derive(Debug, Clone)]
3157pub enum Disk {
3158 Memory(u64),
3160 Differencing(DiskPath),
3162 Persistent(PathBuf),
3164 Temporary(Arc<TempPath>),
3166}
3167
3168#[derive(Debug, Clone)]
3170pub struct PetriVmgsDisk {
3171 pub disk: Disk,
3173 pub encryption_policy: GuestStateEncryptionPolicy,
3175}
3176
3177impl Default for PetriVmgsDisk {
3178 fn default() -> Self {
3179 PetriVmgsDisk {
3180 disk: Disk::Memory(vmgs_format::VMGS_DEFAULT_CAPACITY),
3181 encryption_policy: GuestStateEncryptionPolicy::None(false),
3183 }
3184 }
3185}
3186
3187#[derive(Debug, Clone)]
3189pub enum PetriVmgsResource {
3190 Disk(PetriVmgsDisk),
3192 ReprovisionOnFailure(PetriVmgsDisk),
3194 Reprovision(PetriVmgsDisk),
3196 Ephemeral,
3198}
3199
3200impl PetriVmgsResource {
3201 pub fn vmgs(&self) -> Option<&PetriVmgsDisk> {
3203 match self {
3204 PetriVmgsResource::Disk(vmgs)
3205 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
3206 | PetriVmgsResource::Reprovision(vmgs) => Some(vmgs),
3207 PetriVmgsResource::Ephemeral => None,
3208 }
3209 }
3210
3211 pub fn disk(&self) -> Option<&Disk> {
3213 self.vmgs().map(|vmgs| &vmgs.disk)
3214 }
3215
3216 pub fn encryption_policy(&self) -> Option<GuestStateEncryptionPolicy> {
3218 self.vmgs().map(|vmgs| vmgs.encryption_policy)
3219 }
3220}
3221
3222#[derive(Debug, Clone, Copy)]
3224pub enum PetriGuestStateLifetime {
3225 Disk,
3228 ReprovisionOnFailure,
3230 Reprovision,
3232 Ephemeral,
3234}
3235
3236#[derive(Debug, Clone, Copy)]
3238pub enum SecureBootTemplate {
3239 MicrosoftWindows,
3241 MicrosoftUefiCertificateAuthority,
3243}
3244
3245#[derive(Default, Debug, Clone)]
3248pub struct VmmQuirks {
3249 pub flaky_boot: Option<Duration>,
3252}
3253
3254fn make_vm_safe_name(name: &str) -> String {
3260 const MAX_VM_NAME_LENGTH: usize = 100;
3261 const HASH_LENGTH: usize = 4;
3262 const MAX_PREFIX_LENGTH: usize = MAX_VM_NAME_LENGTH - HASH_LENGTH;
3263
3264 if name.len() <= MAX_VM_NAME_LENGTH {
3265 name.to_owned()
3266 } else {
3267 let mut hasher = DefaultHasher::new();
3269 name.hash(&mut hasher);
3270 let hash = hasher.finish();
3271
3272 let hash_suffix = format!("{:04x}", hash & 0xFFFF);
3274
3275 let truncated = &name[..MAX_PREFIX_LENGTH];
3277 tracing::debug!(
3278 "VM name too long ({}), truncating '{}' to '{}{}'",
3279 name.len(),
3280 name,
3281 truncated,
3282 hash_suffix
3283 );
3284
3285 format!("{}{}", truncated, hash_suffix)
3286 }
3287}
3288
3289#[derive(Debug, Clone, Copy, Eq, PartialEq)]
3291pub enum PetriHaltReason {
3292 PowerOff,
3294 Reset,
3296 Hibernate,
3298 TripleFault,
3300 Other,
3302}
3303
3304impl PetriHaltReason {
3305 pub fn with_detail(self, detail: String) -> PetriHaltReasonDetail {
3307 PetriHaltReasonDetail {
3308 reason: self,
3309 detail,
3310 }
3311 }
3312}
3313
3314#[derive(Debug, Clone)]
3316pub struct PetriHaltReasonDetail {
3317 pub reason: PetriHaltReason,
3319 pub detail: String,
3321}
3322
3323fn append_cmdline(cmd: &mut Option<String>, add_cmd: impl AsRef<str>) {
3324 if let Some(cmd) = cmd.as_mut() {
3325 cmd.push(' ');
3326 cmd.push_str(add_cmd.as_ref());
3327 } else {
3328 *cmd = Some(add_cmd.as_ref().to_string());
3329 }
3330}
3331
3332async fn save_inspect(
3333 name: &str,
3334 inspect: std::pin::Pin<Box<dyn Future<Output = anyhow::Result<inspect::Node>> + Send>>,
3335 log_source: &PetriLogSource,
3336) {
3337 tracing::info!("Collecting {name} inspect details.");
3338 let node = match inspect.await {
3339 Ok(n) => n,
3340 Err(e) => {
3341 tracing::error!(?e, "Failed to get {name}");
3342 return;
3343 }
3344 };
3345 if let Err(e) = log_source.write_attachment(
3346 &format!("timeout_inspect_{name}.log"),
3347 format!("{node:#}").as_bytes(),
3348 ) {
3349 tracing::error!(?e, "Failed to save {name} inspect log");
3350 return;
3351 }
3352 tracing::info!("{name} inspect task finished.");
3353}
3354
3355pub struct ModifyFn<T>(pub Box<dyn FnOnce(T) -> T + Send>);
3357
3358impl<T> Debug for ModifyFn<T> {
3359 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3360 write!(f, "_")
3361 }
3362}
3363
3364fn default_vtl2_settings() -> Vtl2Settings {
3366 Vtl2Settings {
3367 version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
3368 fixed: None,
3369 dynamic: Some(Default::default()),
3370 namespace_settings: Default::default(),
3371 }
3372}
3373
3374#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3376pub enum Vtl {
3377 Vtl0 = 0,
3379 Vtl1 = 1,
3381 Vtl2 = 2,
3383}
3384
3385#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3387pub enum VmbusStorageType {
3388 Scsi,
3390 Nvme,
3392 VirtioBlk,
3394}
3395
3396#[derive(Debug, Clone)]
3398pub struct Drive {
3399 pub disk: Option<Disk>,
3401 pub is_dvd: bool,
3403}
3404
3405impl Drive {
3406 pub fn new(disk: Option<Disk>, is_dvd: bool) -> Self {
3408 Self { disk, is_dvd }
3409 }
3410}
3411
3412#[derive(Debug, Clone)]
3414pub struct VmbusStorageController {
3415 pub target_vtl: Vtl,
3417 pub controller_type: VmbusStorageType,
3419 pub drives: HashMap<u32, Drive>,
3421}
3422
3423impl VmbusStorageController {
3424 pub fn new(target_vtl: Vtl, controller_type: VmbusStorageType) -> Self {
3426 Self {
3427 target_vtl,
3428 controller_type,
3429 drives: HashMap::new(),
3430 }
3431 }
3432
3433 pub fn set_drive(
3435 &mut self,
3436 lun: Option<u32>,
3437 drive: Drive,
3438 allow_modify_existing: bool,
3439 ) -> u32 {
3440 let lun = lun.unwrap_or_else(|| {
3441 let mut lun = None;
3443 for x in 0..u8::MAX as u32 {
3444 if !self.drives.contains_key(&x) {
3445 lun = Some(x);
3446 break;
3447 }
3448 }
3449 lun.expect("all locations on this controller are in use")
3450 });
3451
3452 if self.drives.insert(lun, drive).is_some() && !allow_modify_existing {
3453 panic!("a disk with lun {lun} already existed on this controller");
3454 }
3455
3456 lun
3457 }
3458}
3459
3460pub(crate) fn petri_disk_cache_dir() -> String {
3462 if let Ok(dir) = std::env::var("PETRI_CACHE_DIR") {
3463 return dir;
3464 }
3465
3466 #[cfg(target_os = "macos")]
3467 {
3468 if let Ok(home) = std::env::var("HOME") {
3469 return format!("{home}/Library/Caches/petri");
3470 }
3471 }
3472
3473 #[cfg(windows)]
3474 {
3475 if let Ok(local) = std::env::var("LOCALAPPDATA") {
3476 return format!("{local}\\petri\\cache");
3477 }
3478 }
3479
3480 if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
3482 return format!("{xdg}/petri");
3483 }
3484 if let Ok(home) = std::env::var("HOME") {
3485 return format!("{home}/.cache/petri");
3486 }
3487
3488 ".cache/petri".to_string()
3489}
3490
3491#[cfg(test)]
3492mod tests {
3493 use super::make_vm_safe_name;
3494 use crate::Drive;
3495 use crate::VmbusStorageController;
3496 use crate::VmbusStorageType;
3497 use crate::Vtl;
3498
3499 #[test]
3500 fn test_short_names_unchanged() {
3501 let short_name = "short_test_name";
3502 assert_eq!(make_vm_safe_name(short_name), short_name);
3503 }
3504
3505 #[test]
3506 fn test_exactly_100_chars_unchanged() {
3507 let name_100 = "a".repeat(100);
3508 assert_eq!(make_vm_safe_name(&name_100), name_100);
3509 }
3510
3511 #[test]
3512 fn test_long_name_truncated() {
3513 let long_name = "multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_openhcl_servicing";
3514 let result = make_vm_safe_name(long_name);
3515
3516 assert_eq!(result.len(), 100);
3518
3519 assert!(result.starts_with("multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_ope"));
3521
3522 let suffix = &result[96..];
3524 assert_eq!(suffix.len(), 4);
3525 assert!(u16::from_str_radix(suffix, 16).is_ok());
3527 }
3528
3529 #[test]
3530 fn test_deterministic_results() {
3531 let long_name = "very_long_test_name_that_exceeds_the_100_character_limit_and_should_be_truncated_consistently_every_time";
3532 let result1 = make_vm_safe_name(long_name);
3533 let result2 = make_vm_safe_name(long_name);
3534
3535 assert_eq!(result1, result2);
3536 assert_eq!(result1.len(), 100);
3537 }
3538
3539 #[test]
3540 fn test_different_names_different_hashes() {
3541 let name1 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_1";
3542 let name2 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_2";
3543
3544 let result1 = make_vm_safe_name(name1);
3545 let result2 = make_vm_safe_name(name2);
3546
3547 assert_eq!(result1.len(), 100);
3549 assert_eq!(result2.len(), 100);
3550
3551 assert_ne!(result1, result2);
3553 assert_ne!(&result1[96..], &result2[96..]);
3554 }
3555
3556 #[test]
3557 fn test_vmbus_storage_controller() {
3558 let mut controller = VmbusStorageController::new(Vtl::Vtl0, VmbusStorageType::Scsi);
3559 assert_eq!(
3560 controller.set_drive(Some(1), Drive::new(None, false), false),
3561 1
3562 );
3563 assert!(controller.drives.contains_key(&1));
3564 assert_eq!(
3565 controller.set_drive(None, Drive::new(None, false), false),
3566 0
3567 );
3568 assert!(controller.drives.contains_key(&0));
3569 assert_eq!(
3570 controller.set_drive(None, Drive::new(None, false), false),
3571 2
3572 );
3573 assert!(controller.drives.contains_key(&2));
3574 assert_eq!(
3575 controller.set_drive(Some(0), Drive::new(None, false), true),
3576 0
3577 );
3578 assert!(controller.drives.contains_key(&0));
3579 }
3580}