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 pcie_boot_port: Option<String>,
175
176 minimal_mode: bool,
178 pipette_binary: Option<ResolvedArtifact>,
180 enable_serial: bool,
182 enable_screenshots: bool,
184 prebuilt_initrd: Option<PathBuf>,
186 use_virtio_vsock: bool,
188 #[cfg(target_os = "linux")]
190 vhost_vsock_guest_cid: Option<u32>,
191 no_vmbus: bool,
193}
194
195impl<T: PetriVmmBackend> Debug for PetriVmBuilder<T> {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 f.debug_struct("PetriVmBuilder")
198 .field("backend", &self.backend)
199 .field("config", &self.config)
200 .field("modify_vmm_config", &self.modify_vmm_config.is_some())
201 .field("resources", &self.resources)
202 .field("guest_quirks", &self.guest_quirks)
203 .field("vmm_quirks", &self.vmm_quirks)
204 .field("expected_boot_event", &self.expected_boot_event)
205 .field("override_expect_reset", &self.override_expect_reset)
206 .field("agent_image", &self.agent_image)
207 .field("openhcl_agent_image", &self.openhcl_agent_image)
208 .field("boot_device_type", &self.boot_device_type)
209 .field("pcie_boot_port", &self.pcie_boot_port)
210 .field("minimal_mode", &self.minimal_mode)
211 .field("enable_serial", &self.enable_serial)
212 .field("enable_screenshots", &self.enable_screenshots)
213 .field("prebuilt_initrd", &self.prebuilt_initrd)
214 .field("use_virtio_vsock", &self.use_virtio_vsock)
215 .field("no_vmbus", &self.no_vmbus)
216 .finish()
217 }
218}
219
220#[derive(Debug)]
222pub struct PetriVmConfig {
223 pub name: String,
225 pub arch: MachineArch,
227 pub host_log_levels: Option<OpenvmmLogConfig>,
229 pub firmware: Firmware,
231 pub memory: MemoryConfig,
233 pub proc_topology: ProcessorTopology,
235 pub vmgs: PetriVmgsResource,
237 pub tpm: Option<TpmConfig>,
239 pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
241 pub pcie_nvme_drives: Vec<PcieNvmeDrive>,
243 pub pcie_virtio_blk_drives: Vec<PcieVirtioBlkDrive>,
245 pub physical_nvme_devices: HashMap<Guid, PhysicalNvmeDevice>,
247}
248
249#[derive(Debug)]
251pub struct PcieNvmeDrive {
252 pub port_name: String,
254 pub nsid: u32,
256 pub drive: Drive,
258}
259
260#[derive(Debug)]
262pub struct PcieVirtioBlkDrive {
263 pub port_name: String,
265 pub drive: Drive,
267}
268
269#[derive(Debug, Clone)]
272pub struct PhysicalNvmeDevice {
273 pub target_vtl: Vtl,
275 pub nsid: u32,
277 pub namespace_size_mib: u64,
279}
280
281pub struct PetriVmProperties {
284 pub is_openhcl: bool,
286 pub is_isolated: bool,
288 pub is_pcat: bool,
290 pub is_linux_direct: bool,
292 pub using_vtl0_pipette: bool,
294 pub using_vpci: bool,
296 pub os_flavor: OsFlavor,
298 pub minimal_mode: bool,
300 pub uses_pipette_as_init: bool,
302 pub enable_serial: bool,
304 pub prebuilt_initrd: Option<PathBuf>,
306 pub has_agent_disk: bool,
308 pub use_virtio_vsock: bool,
310 #[cfg(target_os = "linux")]
312 pub vhost_vsock_guest_cid: Option<u32>,
313 pub no_vmbus: bool,
315}
316
317pub struct PetriVmRuntimeConfig {
319 pub vtl2_settings: Option<Vtl2Settings>,
321 pub ide_controllers: Option<[[Option<Drive>; 2]; 2]>,
323 pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
325}
326
327#[derive(Debug)]
329pub struct PetriVmResources {
330 driver: DefaultDriver,
331 log_source: PetriLogSource,
332}
333
334#[async_trait]
336pub trait PetriVmmBackend: Debug {
337 type VmmConfig;
339
340 type VmRuntime: PetriVmRuntime;
342
343 fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool;
346
347 fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks);
349
350 fn default_servicing_flags() -> OpenHclServicingFlags;
352
353 fn create_guest_dump_disk() -> anyhow::Result<
356 Option<(
357 Arc<TempPath>,
358 Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
359 )>,
360 >;
361
362 fn new(resolver: &ArtifactResolver<'_>) -> Self;
364
365 async fn run(
367 self,
368 config: PetriVmConfig,
369 modify_vmm_config: Option<ModifyFn<Self::VmmConfig>>,
370 resources: &PetriVmResources,
371 properties: PetriVmProperties,
372 ) -> anyhow::Result<(Self::VmRuntime, PetriVmRuntimeConfig)>;
373}
374
375pub(crate) const PETRI_IDE_BOOT_CONTROLLER_NUMBER: u32 = 0;
377pub(crate) const PETRI_IDE_BOOT_LUN: u8 = 0;
378pub(crate) const PETRI_IDE_BOOT_CONTROLLER: Guid =
379 guid::guid!("ca56751f-e643-4bef-bf54-f73678e8b7b5");
380
381pub(crate) const PETRI_SCSI_BOOT_LUN: u32 = 0;
383pub(crate) const PETRI_SCSI_PIPETTE_LUN: u32 = 1;
384pub(crate) const PETRI_SCSI_CRASH_LUN: u32 = 2;
385pub(crate) const PETRI_SCSI_VTL0_CONTROLLER: Guid =
387 guid::guid!("27b553e8-8b39-411b-a55f-839971a7884f");
388pub(crate) const PETRI_SCSI_VTL2_CONTROLLER: Guid =
390 guid::guid!("766e96f8-2ceb-437e-afe3-a93169e48a7c");
391pub(crate) const PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER: Guid =
393 guid::guid!("6c474f47-ed39-49e6-bbb9-142177a1da6e");
394
395pub(crate) const PETRI_NVME_BOOT_NSID: u32 = 37;
397pub(crate) const PETRI_NVME_BOOT_VTL0_CONTROLLER: Guid =
399 guid::guid!("e23a04e2-90f5-4852-bc9d-e7ac691b756c");
400pub(crate) const PETRI_NVME_BOOT_VTL2_CONTROLLER: Guid =
402 guid::guid!("92bc8346-718b-449a-8751-edbf3dcd27e4");
403
404pub(crate) const PETRI_PCIE_NVME_AGENT_PORT: &str = "s0rc0rp1";
406pub(crate) const PETRI_PCIE_NVME_AGENT_NSID: u32 = 1;
408
409pub struct PetriVm<T: PetriVmmBackend> {
411 resources: PetriVmResources,
412 runtime: T::VmRuntime,
413 watchdog_tasks: Vec<Task<()>>,
414 openhcl_diag_handler: Option<OpenHclDiagHandler>,
415
416 arch: MachineArch,
417 guest_quirks: GuestQuirksInner,
418 vmm_quirks: VmmQuirks,
419 expected_boot_event: Option<FirmwareEvent>,
420
421 config: PetriVmRuntimeConfig,
422}
423
424impl<T: PetriVmmBackend> PetriVmBuilder<T> {
425 pub fn new(
427 params: PetriTestParams<'_>,
428 artifacts: PetriVmArtifacts<T>,
429 driver: &DefaultDriver,
430 ) -> anyhow::Result<Self> {
431 let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
432 let expected_boot_event = artifacts.firmware.expected_boot_event();
433 let boot_device_type = match artifacts.firmware {
434 Firmware::LinuxDirect { .. } => BootDeviceType::None,
435 Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
436 Firmware::Pcat { .. } => BootDeviceType::Ide,
437 Firmware::OpenhclPcat { .. } => BootDeviceType::IdeViaScsi,
438 Firmware::Uefi {
439 guest: UefiGuest::None,
440 ..
441 }
442 | Firmware::OpenhclUefi {
443 guest: UefiGuest::None,
444 ..
445 } => BootDeviceType::None,
446 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
447 };
448
449 Ok(Self {
450 backend: artifacts.backend,
451 config: PetriVmConfig {
452 name: make_vm_safe_name(params.test_name),
453 arch: artifacts.arch,
454 host_log_levels: None,
455 firmware: artifacts.firmware,
456 memory: Default::default(),
457 proc_topology: Default::default(),
458
459 vmgs: PetriVmgsResource::Ephemeral,
460 tpm: None,
461 vmbus_storage_controllers: HashMap::new(),
462 pcie_nvme_drives: Vec::new(),
463 pcie_virtio_blk_drives: Vec::new(),
464 physical_nvme_devices: HashMap::new(),
465 },
466 modify_vmm_config: None,
467 resources: PetriVmResources {
468 driver: driver.clone(),
469 log_source: params.logger.clone(),
470 },
471
472 guest_quirks,
473 vmm_quirks,
474 expected_boot_event,
475 override_expect_reset: false,
476
477 agent_image: artifacts.agent_image,
478 openhcl_agent_image: artifacts.openhcl_agent_image,
479 boot_device_type,
480 pcie_boot_port: None,
481
482 minimal_mode: false,
483 pipette_binary: artifacts.pipette_binary,
484 enable_serial: true,
485 enable_screenshots: true,
486 prebuilt_initrd: None,
487 use_virtio_vsock: false,
488 #[cfg(target_os = "linux")]
489 vhost_vsock_guest_cid: None,
490 no_vmbus: false,
491 }
492 .add_petri_scsi_controllers()
493 .add_guest_crash_disk(params.post_test_hooks))
494 }
495
496 pub fn minimal(
507 params: PetriTestParams<'_>,
508 artifacts: PetriVmArtifacts<T>,
509 driver: &DefaultDriver,
510 ) -> anyhow::Result<Self> {
511 let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
512 let expected_boot_event = artifacts.firmware.expected_boot_event();
513 let boot_device_type = match artifacts.firmware {
514 Firmware::LinuxDirect { .. } => BootDeviceType::None,
515 Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
516 Firmware::Pcat { .. } => BootDeviceType::Ide,
517 Firmware::OpenhclPcat { .. } => BootDeviceType::IdeViaScsi,
518 Firmware::Uefi {
519 guest: UefiGuest::None,
520 ..
521 }
522 | Firmware::OpenhclUefi {
523 guest: UefiGuest::None,
524 ..
525 } => BootDeviceType::None,
526 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
527 };
528
529 Ok(Self {
530 backend: artifacts.backend,
531 config: PetriVmConfig {
532 name: make_vm_safe_name(params.test_name),
533 arch: artifacts.arch,
534 host_log_levels: None,
535 firmware: artifacts.firmware,
536 memory: Default::default(),
537 proc_topology: Default::default(),
538
539 vmgs: PetriVmgsResource::Ephemeral,
540 tpm: None,
541 vmbus_storage_controllers: HashMap::new(),
542 pcie_nvme_drives: Vec::new(),
543 pcie_virtio_blk_drives: Vec::new(),
544 physical_nvme_devices: HashMap::new(),
545 },
546 modify_vmm_config: None,
547 resources: PetriVmResources {
548 driver: driver.clone(),
549 log_source: params.logger.clone(),
550 },
551
552 guest_quirks,
553 vmm_quirks,
554 expected_boot_event,
555 override_expect_reset: false,
556
557 agent_image: artifacts.agent_image,
558 openhcl_agent_image: artifacts.openhcl_agent_image,
559 boot_device_type,
560 pcie_boot_port: None,
561
562 minimal_mode: true,
563 pipette_binary: artifacts.pipette_binary,
564 enable_serial: false,
565 enable_screenshots: true,
566 prebuilt_initrd: None,
567 use_virtio_vsock: false,
568 #[cfg(target_os = "linux")]
569 vhost_vsock_guest_cid: None,
570 no_vmbus: false,
571 })
572 }
573
574 pub fn is_minimal(&self) -> bool {
576 self.minimal_mode
577 }
578
579 pub fn with_prebuilt_initrd(mut self, path: PathBuf) -> Self {
586 self.prebuilt_initrd = Some(path);
587 self
588 }
589
590 pub fn prepare_initrd(&self) -> anyhow::Result<TempPath> {
601 use anyhow::Context;
602 use std::io::Write;
603
604 let initrd_path = self
605 .config
606 .firmware
607 .linux_direct_initrd()
608 .context("prepare_initrd requires Linux direct boot with initrd")?;
609 let pipette_path = self
610 .pipette_binary
611 .as_ref()
612 .context("prepare_initrd requires a pipette binary")?;
613
614 let initrd_gz = std::fs::read(initrd_path)
615 .with_context(|| format!("failed to read initrd at {}", initrd_path.display()))?;
616 let pipette_data = std::fs::read(pipette_path.get()).with_context(|| {
617 format!(
618 "failed to read pipette binary at {}",
619 pipette_path.get().display()
620 )
621 })?;
622
623 let merged_gz =
624 initrd_cpio::inject_into_initrd(&initrd_gz, "pipette", &pipette_data, 0o100755)
625 .context("failed to inject pipette into initrd")?;
626
627 let mut tmp = tempfile::NamedTempFile::new()
628 .context("failed to create temp file for pre-built initrd")?;
629 tmp.write_all(&merged_gz)
630 .context("failed to write pre-built initrd")?;
631
632 Ok(tmp.into_temp_path())
633 }
634
635 pub fn with_serial_output(mut self) -> Self {
644 self.enable_serial = true;
645 self
646 }
647
648 pub fn without_serial_output(mut self) -> Self {
653 self.enable_serial = false;
654 self
655 }
656
657 pub fn without_screenshots(mut self) -> Self {
662 self.enable_screenshots = false;
663 self
664 }
665
666 pub fn with_virtio_vsock(mut self) -> Self {
677 self.use_virtio_vsock = true;
678 #[cfg(target_os = "linux")]
679 {
680 self.vhost_vsock_guest_cid = None;
681 }
682 self
683 }
684
685 #[cfg(target_os = "linux")]
691 pub fn with_vhost_vsock(mut self, guest_cid: u32) -> Self {
692 assert!(
693 (3..u32::MAX).contains(&guest_cid),
694 "vhost-vsock guest CID must be between 3 and {}",
695 u32::MAX - 1
696 );
697 self.use_virtio_vsock = true;
698 self.vhost_vsock_guest_cid = Some(guest_cid);
699 self
700 }
701
702 pub fn with_no_vmbus(mut self) -> Self {
710 self.no_vmbus = true;
711 if self.config.firmware.os_flavor() != OsFlavor::Windows {
712 self.use_virtio_vsock = true;
713 }
714 self.config.vmbus_storage_controllers.clear();
715 self
716 }
717
718 fn add_petri_scsi_controllers(self) -> Self {
719 let builder = self.add_vmbus_storage_controller(
720 &PETRI_SCSI_VTL0_CONTROLLER,
721 Vtl::Vtl0,
722 VmbusStorageType::Scsi,
723 );
724
725 if builder.is_openhcl() {
726 builder.add_vmbus_storage_controller(
727 &PETRI_SCSI_VTL2_CONTROLLER,
728 Vtl::Vtl2,
729 VmbusStorageType::Scsi,
730 )
731 } else {
732 builder
733 }
734 }
735
736 fn add_guest_crash_disk(self, post_test_hooks: &mut Vec<PetriPostTestHook>) -> Self {
737 let logger = self.resources.log_source.clone();
738 let (disk, disk_hook) = matches!(
739 self.config.firmware.os_flavor(),
740 OsFlavor::Windows | OsFlavor::Linux
741 )
742 .then(|| T::create_guest_dump_disk().expect("failed to create guest dump disk"))
743 .flatten()
744 .unzip();
745
746 if let Some(disk_hook) = disk_hook {
747 post_test_hooks.push(PetriPostTestHook::new(
748 "extract guest crash dumps".into(),
749 move |test_passed| {
750 if test_passed {
751 return Ok(());
752 }
753 let mut disk = disk_hook()?;
754 let gpt = gptman::GPT::read_from(&mut disk, SECTOR_SIZE)?;
755 let partition = fscommon::StreamSlice::new(
756 &mut disk,
757 gpt[1].starting_lba * SECTOR_SIZE,
758 gpt[1].ending_lba * SECTOR_SIZE,
759 )?;
760 let fs = fatfs::FileSystem::new(partition, fatfs::FsOptions::new())?;
761 for entry in fs.root_dir().iter() {
762 let Ok(entry) = entry else {
763 tracing::warn!(?entry, "failed to read entry in guest crash dump disk");
764 continue;
765 };
766 if !entry.is_file() {
767 tracing::warn!(
768 ?entry,
769 "skipping non-file entry in guest crash dump disk"
770 );
771 continue;
772 }
773 logger.write_attachment(&entry.file_name(), entry.to_file())?;
774 }
775 Ok(())
776 },
777 ));
778 }
779
780 if let Some(disk) = disk {
781 self.add_vmbus_drive(
782 Drive::new(Some(Disk::Temporary(disk)), false),
783 &PETRI_SCSI_VTL0_CONTROLLER,
784 Some(PETRI_SCSI_CRASH_LUN),
785 )
786 } else {
787 self
788 }
789 }
790
791 fn add_agent_disks(self) -> Self {
792 self.add_agent_disk_inner(Vtl::Vtl0)
793 .add_agent_disk_inner(Vtl::Vtl2)
794 }
795
796 fn add_agent_disk_inner(mut self, target_vtl: Vtl) -> Self {
797 let (agent_image, controller_id) = match target_vtl {
798 Vtl::Vtl0 => (self.agent_image.as_ref(), PETRI_SCSI_VTL0_CONTROLLER),
799 Vtl::Vtl1 => panic!("no VTL1 agent disk"),
800 Vtl::Vtl2 => (
801 self.openhcl_agent_image.as_ref(),
802 PETRI_SCSI_VTL2_CONTROLLER,
803 ),
804 };
805
806 if target_vtl == Vtl::Vtl0
809 && self.uses_pipette_as_init()
810 && !agent_image.is_some_and(|i| i.has_extras())
811 {
812 return self;
813 }
814
815 let Some(agent_disk) = agent_image.and_then(|i| {
816 i.build(crate::disk_image::ImageType::Vhd)
817 .expect("failed to build agent image")
818 }) else {
819 return self;
820 };
821
822 if self.no_vmbus {
825 self.config.pcie_nvme_drives.push(PcieNvmeDrive {
826 port_name: PETRI_PCIE_NVME_AGENT_PORT.into(),
827 nsid: PETRI_PCIE_NVME_AGENT_NSID,
828 drive: Drive::new(
829 Some(Disk::Temporary(Arc::new(agent_disk.into_temp_path()))),
830 false,
831 ),
832 });
833 return self;
834 }
835
836 if !self
839 .config
840 .vmbus_storage_controllers
841 .contains_key(&controller_id)
842 {
843 self = self.add_vmbus_storage_controller(
844 &controller_id,
845 target_vtl,
846 VmbusStorageType::Scsi,
847 );
848 }
849
850 self.add_vmbus_drive(
851 Drive::new(
852 Some(Disk::Temporary(Arc::new(agent_disk.into_temp_path()))),
853 false,
854 ),
855 &controller_id,
856 Some(PETRI_SCSI_PIPETTE_LUN),
857 )
858 }
859
860 fn add_boot_disk(mut self) -> Self {
861 if self.boot_device_type.requires_vtl2() && !self.is_openhcl() {
862 panic!("boot device type {:?} requires vtl2", self.boot_device_type);
863 }
864
865 if self.no_vmbus && self.boot_device_type.requires_vmbus() {
866 panic!(
867 "boot device type {:?} requires vmbus, but vmbus is disabled; \
868 use with_boot_device_type(BootDeviceType::PcieNvme) or similar",
869 self.boot_device_type
870 );
871 }
872
873 if self.boot_device_type.requires_vpci_boot() {
874 self.config
875 .firmware
876 .uefi_config_mut()
877 .expect("vpci boot requires uefi")
878 .enable_vpci_boot = true;
879 }
880
881 if let Some(boot_drive) = self.config.firmware.boot_drive() {
882 match self.boot_device_type {
883 BootDeviceType::None => unreachable!(),
884 BootDeviceType::Ide => self.add_ide_drive(
885 boot_drive,
886 PETRI_IDE_BOOT_CONTROLLER_NUMBER,
887 PETRI_IDE_BOOT_LUN,
888 ),
889 BootDeviceType::IdeViaScsi => self
890 .add_vmbus_drive(
891 boot_drive,
892 &PETRI_SCSI_VTL2_CONTROLLER,
893 Some(PETRI_SCSI_BOOT_LUN),
894 )
895 .add_vtl2_storage_controller(
896 Vtl2StorageControllerBuilder::new(ControllerType::Ide)
897 .with_instance_id(PETRI_IDE_BOOT_CONTROLLER)
898 .add_lun(
899 Vtl2LunBuilder::disk()
900 .with_channel(PETRI_IDE_BOOT_CONTROLLER_NUMBER)
901 .with_location(PETRI_IDE_BOOT_LUN as u32)
902 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
903 ControllerType::Scsi,
904 PETRI_SCSI_VTL2_CONTROLLER,
905 PETRI_SCSI_BOOT_LUN,
906 )),
907 )
908 .build(),
909 ),
910 BootDeviceType::IdeViaNvme => todo!(),
911 BootDeviceType::Scsi => self.add_vmbus_drive(
912 boot_drive,
913 &PETRI_SCSI_VTL0_CONTROLLER,
914 Some(PETRI_SCSI_BOOT_LUN),
915 ),
916 BootDeviceType::ScsiViaScsi => self
917 .add_vmbus_drive(
918 boot_drive,
919 &PETRI_SCSI_VTL2_CONTROLLER,
920 Some(PETRI_SCSI_BOOT_LUN),
921 )
922 .add_vtl2_storage_controller(
923 Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
924 .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
925 .add_lun(
926 Vtl2LunBuilder::disk()
927 .with_location(PETRI_SCSI_BOOT_LUN)
928 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
929 ControllerType::Scsi,
930 PETRI_SCSI_VTL2_CONTROLLER,
931 PETRI_SCSI_BOOT_LUN,
932 )),
933 )
934 .build(),
935 ),
936 BootDeviceType::ScsiViaNvme => self
937 .add_vmbus_storage_controller(
938 &PETRI_NVME_BOOT_VTL2_CONTROLLER,
939 Vtl::Vtl2,
940 VmbusStorageType::Nvme,
941 )
942 .add_vmbus_drive(
943 boot_drive,
944 &PETRI_NVME_BOOT_VTL2_CONTROLLER,
945 Some(PETRI_NVME_BOOT_NSID),
946 )
947 .add_vtl2_storage_controller(
948 Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
949 .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
950 .add_lun(
951 Vtl2LunBuilder::disk()
952 .with_location(PETRI_SCSI_BOOT_LUN)
953 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
954 ControllerType::Nvme,
955 PETRI_NVME_BOOT_VTL2_CONTROLLER,
956 PETRI_NVME_BOOT_NSID,
957 )),
958 )
959 .build(),
960 ),
961 BootDeviceType::Nvme => self
962 .add_vmbus_storage_controller(
963 &PETRI_NVME_BOOT_VTL0_CONTROLLER,
964 Vtl::Vtl0,
965 VmbusStorageType::Nvme,
966 )
967 .add_vmbus_drive(
968 boot_drive,
969 &PETRI_NVME_BOOT_VTL0_CONTROLLER,
970 Some(PETRI_NVME_BOOT_NSID),
971 ),
972 BootDeviceType::NvmeViaScsi => todo!(),
973 BootDeviceType::NvmeViaNvme => todo!(),
974 BootDeviceType::PcieNvme => {
975 let port_name = self
976 .pcie_boot_port
977 .clone()
978 .unwrap_or_else(|| "s0rc0rp0".into());
979 self.config.pcie_nvme_drives.push(PcieNvmeDrive {
980 port_name,
981 nsid: 1,
982 drive: boot_drive,
983 });
984 self
985 }
986 BootDeviceType::PcieVirtioBlk => {
987 self.config.pcie_virtio_blk_drives.push(PcieVirtioBlkDrive {
988 port_name: "s0rc0rp0".into(),
989 drive: boot_drive,
990 });
991 self
992 }
993 }
994 } else {
995 self
996 }
997 }
998
999 fn has_agent_disk(&self) -> bool {
1004 if self.uses_pipette_as_init() {
1005 self.agent_image.as_ref().is_some_and(|i| i.has_extras())
1006 } else {
1007 self.agent_image.is_some()
1008 }
1009 }
1010
1011 pub fn properties(&self) -> PetriVmProperties {
1013 PetriVmProperties {
1014 is_openhcl: self.config.firmware.is_openhcl(),
1015 is_isolated: self.config.firmware.isolation().is_some(),
1016 is_pcat: self.config.firmware.is_pcat(),
1017 is_linux_direct: self.config.firmware.is_linux_direct(),
1018 using_vtl0_pipette: self.using_vtl0_pipette(),
1019 using_vpci: self.boot_device_type.requires_vpci_boot(),
1020 os_flavor: self.config.firmware.os_flavor(),
1021 minimal_mode: self.minimal_mode,
1022 uses_pipette_as_init: self.uses_pipette_as_init(),
1023 enable_serial: self.enable_serial,
1024 prebuilt_initrd: self.prebuilt_initrd.clone(),
1025 has_agent_disk: self.has_agent_disk(),
1026 use_virtio_vsock: self.use_virtio_vsock,
1027 #[cfg(target_os = "linux")]
1028 vhost_vsock_guest_cid: self.vhost_vsock_guest_cid,
1029 no_vmbus: self.no_vmbus,
1030 }
1031 }
1032
1033 fn uses_pipette_as_init(&self) -> bool {
1039 self.config.firmware.is_linux_direct()
1040 && !self.config.firmware.is_openhcl()
1041 && self.pipette_binary.is_some()
1042 }
1043
1044 pub fn using_vtl0_pipette(&self) -> bool {
1046 self.uses_pipette_as_init()
1047 || self
1048 .agent_image
1049 .as_ref()
1050 .is_some_and(|x| x.contains_pipette())
1051 }
1052
1053 pub async fn run_without_agent(self) -> anyhow::Result<PetriVm<T>> {
1057 self.run_core().await
1058 }
1059
1060 pub async fn run(self) -> anyhow::Result<(PetriVm<T>, PipetteClient)> {
1063 assert!(self.using_vtl0_pipette());
1064
1065 let mut vm = self.run_core().await?;
1066 let client = vm.wait_for_agent().await?;
1067 Ok((vm, client))
1068 }
1069
1070 async fn run_core(mut self) -> anyhow::Result<PetriVm<T>> {
1071 self = self.add_boot_disk().add_agent_disks();
1074
1075 let _prepared_initrd_guard;
1079 if self.uses_pipette_as_init() && self.prebuilt_initrd.is_none() {
1080 let tmp = self.prepare_initrd()?;
1081 self.prebuilt_initrd = Some(tmp.to_path_buf());
1082 _prepared_initrd_guard = Some(tmp);
1083 } else {
1084 _prepared_initrd_guard = None;
1085 }
1086
1087 tracing::debug!(builder = ?self);
1088
1089 let arch = self.config.arch;
1090 let expect_reset = self.expect_reset();
1091 let properties = self.properties();
1092
1093 let (mut runtime, config) = self
1094 .backend
1095 .run(
1096 self.config,
1097 self.modify_vmm_config,
1098 &self.resources,
1099 properties,
1100 )
1101 .await?;
1102 let openhcl_diag_handler = runtime.openhcl_diag();
1103 let watchdog_tasks =
1104 Self::start_watchdog_tasks(&self.resources, &mut runtime, self.enable_screenshots)?;
1105
1106 let mut vm = PetriVm {
1107 resources: self.resources,
1108 runtime,
1109 watchdog_tasks,
1110 openhcl_diag_handler,
1111
1112 arch,
1113 guest_quirks: self.guest_quirks,
1114 vmm_quirks: self.vmm_quirks,
1115 expected_boot_event: self.expected_boot_event,
1116
1117 config,
1118 };
1119
1120 if expect_reset {
1121 vm.wait_for_reset_core().await?;
1122 }
1123
1124 vm.wait_for_expected_boot_event().await?;
1125
1126 Ok(vm)
1127 }
1128
1129 fn expect_reset(&self) -> bool {
1130 self.override_expect_reset
1131 || matches!(
1132 (
1133 self.guest_quirks.initial_reboot,
1134 self.expected_boot_event,
1135 &self.config.firmware,
1136 &self.config.tpm,
1137 ),
1138 (
1139 Some(InitialRebootCondition::Always),
1140 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
1141 _,
1142 _,
1143 ) | (
1144 Some(InitialRebootCondition::WithTpm),
1145 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
1146 _,
1147 Some(_),
1148 )
1149 )
1150 }
1151
1152 fn start_watchdog_tasks(
1153 resources: &PetriVmResources,
1154 runtime: &mut T::VmRuntime,
1155 enable_screenshots: bool,
1156 ) -> anyhow::Result<Vec<Task<()>>> {
1157 let mut tasks = Vec::new();
1158
1159 {
1160 const TIMEOUT_DURATION_MINUTES: u64 = 10;
1161 const TIMER_DURATION: Duration = Duration::from_secs(TIMEOUT_DURATION_MINUTES * 60);
1162 let log_source = resources.log_source.clone();
1163 let inspect_task =
1164 |name,
1165 driver: &DefaultDriver,
1166 inspect: std::pin::Pin<Box<dyn Future<Output = _> + Send>>| {
1167 driver.spawn(format!("petri-watchdog-inspect-{name}"), async move {
1168 if CancelContext::new()
1169 .with_timeout(Duration::from_secs(10))
1170 .until_cancelled(save_inspect(name, inspect, &log_source))
1171 .await
1172 .is_err()
1173 {
1174 tracing::warn!(name, "Failed to collect inspect data within timeout");
1175 }
1176 })
1177 };
1178
1179 let driver = resources.driver.clone();
1180 let vmm_inspector = runtime.inspector();
1181 let openhcl_diag_handler = runtime.openhcl_diag();
1182 tasks.push(resources.driver.spawn("timer-watchdog", async move {
1183 PolledTimer::new(&driver).sleep(TIMER_DURATION).await;
1184 tracing::warn!("Test timeout reached after {TIMEOUT_DURATION_MINUTES} minutes, collecting diagnostics.");
1185 let mut timeout_tasks = Vec::new();
1186 if let Some(inspector) = vmm_inspector {
1187 timeout_tasks.push(inspect_task.clone()("vmm", &driver, Box::pin(async move { inspector.inspect("").await })) );
1188 }
1189 if let Some(openhcl_diag_handler) = openhcl_diag_handler {
1190 timeout_tasks.push(inspect_task("openhcl", &driver, Box::pin(async move { openhcl_diag_handler.inspect("", None, None).await })));
1191 }
1192 futures::future::join_all(timeout_tasks).await;
1193 tracing::error!("Test time out diagnostics collection complete, aborting.");
1194 panic!("Test timed out");
1195 }));
1196 }
1197
1198 if enable_screenshots {
1199 if let Some(mut framebuffer_access) = runtime.take_framebuffer_access() {
1200 let mut timer = PolledTimer::new(&resources.driver);
1201 let log_source = resources.log_source.clone();
1202
1203 tasks.push(
1204 resources
1205 .driver
1206 .spawn("petri-watchdog-screenshot", async move {
1207 let mut image = Vec::new();
1208 let mut last_image = Vec::new();
1209 loop {
1210 timer.sleep(Duration::from_secs(2)).await;
1211 tracing::trace!("Taking screenshot.");
1212
1213 let VmScreenshotMeta {
1214 color,
1215 width,
1216 height,
1217 } = match framebuffer_access.screenshot(&mut image).await {
1218 Ok(Some(meta)) => meta,
1219 Ok(None) => {
1220 tracing::debug!("VM off, skipping screenshot.");
1221 continue;
1222 }
1223 Err(e) => {
1224 tracing::error!(?e, "Failed to take screenshot");
1225 continue;
1226 }
1227 };
1228
1229 if image == last_image {
1230 tracing::debug!(
1231 "No change in framebuffer, skipping screenshot."
1232 );
1233 continue;
1234 }
1235
1236 let r = log_source.create_attachment("screenshot.png").and_then(
1237 |mut f| {
1238 image::write_buffer_with_format(
1239 &mut f,
1240 &image,
1241 width.into(),
1242 height.into(),
1243 color,
1244 image::ImageFormat::Png,
1245 )
1246 .map_err(Into::into)
1247 },
1248 );
1249
1250 if let Err(e) = r {
1251 tracing::error!(?e, "Failed to save screenshot");
1252 } else {
1253 tracing::info!("Screenshot saved.");
1254 }
1255
1256 std::mem::swap(&mut image, &mut last_image);
1257 }
1258 }),
1259 );
1260 }
1261 }
1262
1263 Ok(tasks)
1264 }
1265
1266 pub fn with_expect_boot_failure(mut self) -> Self {
1269 self.expected_boot_event = Some(FirmwareEvent::BootFailed);
1270 self
1271 }
1272
1273 pub fn with_expect_no_boot_event(mut self) -> Self {
1276 self.expected_boot_event = None;
1277 self
1278 }
1279
1280 pub fn with_expect_reset(mut self) -> Self {
1284 self.override_expect_reset = true;
1285 self
1286 }
1287
1288 pub fn with_secure_boot(mut self) -> Self {
1290 self.config
1291 .firmware
1292 .uefi_config_mut()
1293 .expect("Secure boot is only supported for UEFI firmware.")
1294 .secure_boot_enabled = true;
1295
1296 match self.os_flavor() {
1297 OsFlavor::Windows => self.with_windows_secure_boot_template(),
1298 OsFlavor::Linux => self.with_uefi_ca_secure_boot_template(),
1299 _ => panic!(
1300 "Secure boot unsupported for OS flavor {:?}",
1301 self.os_flavor()
1302 ),
1303 }
1304 }
1305
1306 pub fn with_windows_secure_boot_template(mut self) -> Self {
1308 self.config
1309 .firmware
1310 .uefi_config_mut()
1311 .expect("Secure boot is only supported for UEFI firmware.")
1312 .secure_boot_template = Some(SecureBootTemplate::MicrosoftWindows);
1313 self
1314 }
1315
1316 pub fn with_uefi_ca_secure_boot_template(mut self) -> Self {
1318 self.config
1319 .firmware
1320 .uefi_config_mut()
1321 .expect("Secure boot is only supported for UEFI firmware.")
1322 .secure_boot_template = Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority);
1323 self
1324 }
1325
1326 pub fn with_custom_uefi_json(mut self, json: impl Into<Vec<u8>>) -> Self {
1328 self.config
1329 .firmware
1330 .uefi_config_mut()
1331 .expect("Custom UEFI variables are only supported for UEFI firmware.")
1332 .custom_uefi_json = Some(json.into());
1333 self
1334 }
1335
1336 pub fn with_processor_topology(mut self, topology: ProcessorTopology) -> Self {
1338 self.config.proc_topology = topology;
1339 self
1340 }
1341
1342 pub fn with_memory(mut self, memory: MemoryConfig) -> Self {
1344 self.config.memory = memory;
1345 self
1346 }
1347
1348 pub fn with_vtl2_base_address_type(mut self, address_type: Vtl2BaseAddressType) -> Self {
1353 self.config
1354 .firmware
1355 .openhcl_config_mut()
1356 .expect("OpenHCL firmware is required to set custom VTL2 address type.")
1357 .vtl2_base_address_type = Some(address_type);
1358 self
1359 }
1360
1361 pub fn with_custom_openhcl(mut self, artifact: ResolvedArtifact<impl IsOpenhclIgvm>) -> Self {
1363 match &mut self.config.firmware {
1364 Firmware::OpenhclLinuxDirect { igvm_path, .. }
1365 | Firmware::OpenhclPcat { igvm_path, .. }
1366 | Firmware::OpenhclUefi { igvm_path, .. } => {
1367 *igvm_path = artifact.erase();
1368 }
1369 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } | Firmware::Pcat { .. } => {
1370 panic!("Custom OpenHCL is only supported for OpenHCL firmware.")
1371 }
1372 }
1373 self
1374 }
1375
1376 pub fn with_openhcl_command_line(mut self, additional_command_line: &str) -> Self {
1378 append_cmdline(
1379 &mut self
1380 .config
1381 .firmware
1382 .openhcl_config_mut()
1383 .expect("OpenHCL command line is only supported for OpenHCL firmware.")
1384 .custom_command_line,
1385 additional_command_line,
1386 );
1387 self
1388 }
1389
1390 pub fn with_confidential_filtering(self) -> Self {
1392 if !self.config.firmware.is_openhcl() {
1393 panic!("Confidential filtering is only supported for OpenHCL");
1394 }
1395 self.with_openhcl_command_line(&format!(
1396 "{}=1 {}=0",
1397 underhill_confidentiality::OPENHCL_CONFIDENTIAL_ENV_VAR_NAME,
1398 underhill_confidentiality::OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME
1399 ))
1400 }
1401
1402 pub fn with_openhcl_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1404 self.config
1405 .firmware
1406 .openhcl_config_mut()
1407 .expect("OpenHCL firmware is required to set custom OpenHCL log levels.")
1408 .log_levels = levels;
1409 self
1410 }
1411
1412 pub fn with_host_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1416 if let OpenvmmLogConfig::Custom(ref custom_levels) = levels {
1417 for key in custom_levels.keys() {
1418 if !["OPENVMM_LOG", "OPENVMM_SHOW_SPANS"].contains(&key.as_str()) {
1419 panic!("Unsupported OpenVMM log level key: {}", key);
1420 }
1421 }
1422 }
1423
1424 self.config.host_log_levels = Some(levels.clone());
1425 self
1426 }
1427
1428 pub fn with_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
1430 self.agent_image
1431 .as_mut()
1432 .expect("no guest pipette")
1433 .add_file(name, artifact);
1434 self
1435 }
1436
1437 pub fn with_openhcl_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
1439 self.openhcl_agent_image
1440 .as_mut()
1441 .expect("no openhcl pipette")
1442 .add_file(name, artifact);
1443 self
1444 }
1445
1446 pub fn with_uefi_frontpage(mut self, enable: bool) -> Self {
1448 self.config
1449 .firmware
1450 .uefi_config_mut()
1451 .expect("UEFI frontpage is only supported for UEFI firmware.")
1452 .disable_frontpage = !enable;
1453 self
1454 }
1455
1456 pub fn with_efi_diagnostics_log_level(mut self, level: EfiDiagnosticsLogLevel) -> Self {
1462 self.config
1463 .firmware
1464 .uefi_config_mut()
1465 .expect("EFI diagnostics log level is only supported for UEFI firmware.")
1466 .efi_diagnostics_log_level = level;
1467 self
1468 }
1469
1470 pub fn with_efi_diagnostics_rate_limit(mut self, limit: u32) -> Self {
1476 self.config
1477 .firmware
1478 .uefi_config_mut()
1479 .expect("EFI diagnostics rate limit is only supported for UEFI firmware.")
1480 .efi_diagnostics_rate_limit = Some(limit);
1481 self
1482 }
1483
1484 pub fn with_default_boot_always_attempt(mut self, enable: bool) -> Self {
1486 self.config
1487 .firmware
1488 .uefi_config_mut()
1489 .expect("Default boot always attempt is only supported for UEFI firmware.")
1490 .default_boot_always_attempt = enable;
1491 self
1492 }
1493
1494 pub fn with_uefi_force_dma_bounce(mut self, enable: bool) -> Self {
1496 self.config
1497 .firmware
1498 .uefi_config_mut()
1499 .expect("force DMA bounce is only supported for UEFI firmware.")
1500 .force_dma_bounce = enable;
1501 self
1502 }
1503
1504 pub fn with_vmbus_redirect(mut self, enable: bool) -> Self {
1506 self.config
1507 .firmware
1508 .openhcl_config_mut()
1509 .expect("VMBus redirection is only supported for OpenHCL firmware.")
1510 .vmbus_redirect = enable;
1511 self
1512 }
1513
1514 pub fn with_guest_state_lifetime(
1516 mut self,
1517 guest_state_lifetime: PetriGuestStateLifetime,
1518 ) -> Self {
1519 let disk = match self.config.vmgs {
1520 PetriVmgsResource::Disk(disk)
1521 | PetriVmgsResource::ReprovisionOnFailure(disk)
1522 | PetriVmgsResource::Reprovision(disk) => disk,
1523 PetriVmgsResource::Ephemeral => PetriVmgsDisk::default(),
1524 };
1525 self.config.vmgs = match guest_state_lifetime {
1526 PetriGuestStateLifetime::Disk => PetriVmgsResource::Disk(disk),
1527 PetriGuestStateLifetime::ReprovisionOnFailure => {
1528 PetriVmgsResource::ReprovisionOnFailure(disk)
1529 }
1530 PetriGuestStateLifetime::Reprovision => PetriVmgsResource::Reprovision(disk),
1531 PetriGuestStateLifetime::Ephemeral => {
1532 if !matches!(disk.disk, Disk::Memory(_)) {
1533 panic!("attempted to use ephemeral guest state after specifying backing vmgs")
1534 }
1535 PetriVmgsResource::Ephemeral
1536 }
1537 };
1538 self
1539 }
1540
1541 pub fn with_guest_state_encryption(mut self, policy: GuestStateEncryptionPolicy) -> Self {
1543 match &mut self.config.vmgs {
1544 PetriVmgsResource::Disk(vmgs)
1545 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1546 | PetriVmgsResource::Reprovision(vmgs) => {
1547 vmgs.encryption_policy = policy;
1548 }
1549 PetriVmgsResource::Ephemeral => {
1550 panic!("attempted to encrypt ephemeral guest state")
1551 }
1552 }
1553 self
1554 }
1555
1556 pub fn with_initial_vmgs(self, disk: ResolvedArtifact<impl IsTestVmgs>) -> Self {
1558 self.with_backing_vmgs(Disk::Differencing(DiskPath::Local(disk.into())))
1559 }
1560
1561 pub fn with_persistent_vmgs(self, disk: impl AsRef<Path>) -> Self {
1563 self.with_backing_vmgs(Disk::Persistent(disk.as_ref().to_path_buf()))
1564 }
1565
1566 fn with_backing_vmgs(mut self, disk: Disk) -> Self {
1567 match &mut self.config.vmgs {
1568 PetriVmgsResource::Disk(vmgs)
1569 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1570 | PetriVmgsResource::Reprovision(vmgs) => {
1571 if !matches!(vmgs.disk, Disk::Memory(_)) {
1572 panic!("already specified a backing vmgs file");
1573 }
1574 vmgs.disk = disk;
1575 }
1576 PetriVmgsResource::Ephemeral => {
1577 panic!("attempted to specify a backing vmgs with ephemeral guest state")
1578 }
1579 }
1580 self
1581 }
1582
1583 pub fn with_boot_device_type(mut self, boot: BootDeviceType) -> Self {
1587 self.boot_device_type = boot;
1588 self
1589 }
1590
1591 pub fn with_pcie_boot_port(mut self, port_name: &str) -> Self {
1597 self.pcie_boot_port = Some(port_name.to_string());
1598 self
1599 }
1600
1601 pub fn with_tpm(mut self, enable: bool) -> Self {
1603 if enable {
1604 self.config.tpm.get_or_insert_default();
1605 } else {
1606 self.config.tpm = None;
1607 }
1608 self
1609 }
1610
1611 pub fn with_tpm_state_persistence(mut self, tpm_state_persistence: bool) -> Self {
1613 self.config
1614 .tpm
1615 .as_mut()
1616 .expect("TPM persistence requires a TPM")
1617 .no_persistent_secrets = !tpm_state_persistence;
1618 self
1619 }
1620
1621 pub fn with_hardware_sealing_policy(mut self, policy: PetriHardwareSealingPolicy) -> Self {
1623 self.config
1624 .tpm
1625 .as_mut()
1626 .expect("hardware sealing policy requires a TPM")
1627 .hardware_sealing_policy = policy;
1628 self
1629 }
1630
1631 pub fn with_custom_vtl2_settings(
1635 mut self,
1636 f: impl FnOnce(&mut Vtl2Settings) + 'static + Send + Sync,
1637 ) -> Self {
1638 f(self
1639 .config
1640 .firmware
1641 .vtl2_settings()
1642 .expect("Custom VTL 2 settings are only supported with OpenHCL"));
1643 self
1644 }
1645
1646 pub fn add_vtl2_storage_controller(self, controller: StorageController) -> Self {
1648 self.with_custom_vtl2_settings(move |v| {
1649 v.dynamic
1650 .as_mut()
1651 .unwrap()
1652 .storage_controllers
1653 .push(controller)
1654 })
1655 }
1656
1657 pub fn add_vmbus_storage_controller(
1659 mut self,
1660 id: &Guid,
1661 target_vtl: Vtl,
1662 controller_type: VmbusStorageType,
1663 ) -> Self {
1664 if self
1665 .config
1666 .vmbus_storage_controllers
1667 .insert(
1668 *id,
1669 VmbusStorageController::new(target_vtl, controller_type),
1670 )
1671 .is_some()
1672 {
1673 panic!("storage controller {id} already existed");
1674 }
1675 self
1676 }
1677
1678 pub fn add_vmbus_drive(
1680 mut self,
1681 drive: Drive,
1682 controller_id: &Guid,
1683 controller_location: Option<u32>,
1684 ) -> Self {
1685 let controller = self
1686 .config
1687 .vmbus_storage_controllers
1688 .get_mut(controller_id)
1689 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
1690
1691 _ = controller.set_drive(controller_location, drive, false);
1692
1693 self
1694 }
1695
1696 pub fn add_ide_drive(
1698 mut self,
1699 drive: Drive,
1700 controller_number: u32,
1701 controller_location: u8,
1702 ) -> Self {
1703 self.config
1704 .firmware
1705 .ide_controllers_mut()
1706 .expect("Host IDE requires PCAT with no HCL")[controller_number as usize]
1707 [controller_location as usize] = Some(drive);
1708
1709 self
1710 }
1711
1712 pub fn add_physical_nvme_device(mut self, vsid: Guid, device: PhysicalNvmeDevice) -> Self {
1714 if self
1715 .config
1716 .physical_nvme_devices
1717 .insert(vsid, device)
1718 .is_some()
1719 {
1720 panic!("physical NVMe device {vsid} already existed");
1721 }
1722 self
1723 }
1724
1725 pub fn os_flavor(&self) -> OsFlavor {
1727 self.config.firmware.os_flavor()
1728 }
1729
1730 pub fn is_openhcl(&self) -> bool {
1732 self.config.firmware.is_openhcl()
1733 }
1734
1735 pub fn isolation(&self) -> Option<IsolationType> {
1737 self.config.firmware.isolation()
1738 }
1739
1740 pub fn arch(&self) -> MachineArch {
1742 self.config.arch
1743 }
1744
1745 pub fn log_source(&self) -> &PetriLogSource {
1747 &self.resources.log_source
1748 }
1749
1750 pub fn default_servicing_flags(&self) -> OpenHclServicingFlags {
1752 T::default_servicing_flags()
1753 }
1754
1755 pub fn modify_backend(
1757 mut self,
1758 f: impl FnOnce(T::VmmConfig) -> T::VmmConfig + 'static + Send,
1759 ) -> Self {
1760 if self.modify_vmm_config.is_some() {
1761 panic!("only one modify_backend allowed");
1762 }
1763 self.modify_vmm_config = Some(ModifyFn(Box::new(f)));
1764 self
1765 }
1766}
1767
1768impl<T: PetriVmmBackend> PetriVm<T> {
1769 pub async fn teardown(self) -> anyhow::Result<()> {
1771 tracing::info!("Tearing down VM...");
1772 self.runtime.teardown().await
1773 }
1774
1775 pub async fn wait_for_halt(&mut self) -> anyhow::Result<PetriHaltReasonDetail> {
1777 tracing::info!("Waiting for VM to halt...");
1778 let halt_reason = self.runtime.wait_for_halt(false).await?;
1779 tracing::info!("VM halted: {halt_reason:?}. Cancelling watchdogs...");
1780 futures::future::join_all(self.watchdog_tasks.drain(..).map(|t| t.cancel())).await;
1781 Ok(halt_reason)
1782 }
1783
1784 pub async fn wait_for_clean_shutdown(&mut self) -> anyhow::Result<()> {
1786 let halt_reason = self.wait_for_halt().await?;
1787 if halt_reason.reason != PetriHaltReason::PowerOff {
1788 anyhow::bail!("Expected PowerOff, got {halt_reason:?}");
1789 }
1790 tracing::info!("VM was cleanly powered off and torn down.");
1791 Ok(())
1792 }
1793
1794 pub async fn wait_for_teardown(mut self) -> anyhow::Result<PetriHaltReasonDetail> {
1797 let halt_reason = self.wait_for_halt().await?;
1798 self.teardown().await?;
1799 Ok(halt_reason)
1800 }
1801
1802 pub async fn wait_for_clean_teardown(mut self) -> anyhow::Result<()> {
1804 self.wait_for_clean_shutdown().await?;
1805 self.teardown().await
1806 }
1807
1808 pub async fn wait_for_reset_no_agent(&mut self) -> anyhow::Result<()> {
1810 self.wait_for_reset_core().await?;
1811 self.wait_for_expected_boot_event().await?;
1812 Ok(())
1813 }
1814
1815 pub async fn wait_for_reset(&mut self) -> anyhow::Result<PipetteClient> {
1817 self.wait_for_reset_no_agent().await?;
1818 self.wait_for_agent().await
1819 }
1820
1821 async fn wait_for_reset_core(&mut self) -> anyhow::Result<()> {
1822 tracing::info!("Waiting for VM to reset...");
1823 let halt_reason = self.runtime.wait_for_halt(true).await?;
1824 if halt_reason.reason != PetriHaltReason::Reset {
1825 anyhow::bail!("Expected reset, got {halt_reason:?}");
1826 }
1827 tracing::info!("VM reset.");
1828 Ok(())
1829 }
1830
1831 pub async fn inspect_openhcl(
1842 &self,
1843 path: impl Into<String>,
1844 depth: Option<usize>,
1845 timeout: Option<Duration>,
1846 ) -> anyhow::Result<inspect::Node> {
1847 self.openhcl_diag()?
1848 .inspect(path.into().as_str(), depth, timeout)
1849 .await
1850 }
1851
1852 pub async fn inspect_update_openhcl(
1862 &self,
1863 path: impl Into<String>,
1864 value: impl Into<String>,
1865 ) -> anyhow::Result<inspect::Value> {
1866 self.openhcl_diag()?
1867 .inspect_update(path.into(), value.into())
1868 .await
1869 }
1870
1871 pub async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()> {
1873 self.inspect_openhcl("", None, None).await.map(|_| ())
1874 }
1875
1876 pub async fn inspect_vmm(&self, path: &str) -> anyhow::Result<inspect::Node> {
1887 use anyhow::Context;
1888
1889 let inspector = self
1890 .runtime
1891 .inspector()
1892 .context("this VMM backend does not support inspect")?;
1893 inspector.inspect(path).await
1894 }
1895
1896 pub async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()> {
1902 self.openhcl_diag()?.wait_for_vtl2().await
1903 }
1904
1905 pub async fn kmsg(&self) -> anyhow::Result<diag_client::kmsg_stream::KmsgStream> {
1907 self.openhcl_diag()?.kmsg().await
1908 }
1909
1910 pub async fn openhcl_core_dump(&self, name: &str, path: &Path) -> anyhow::Result<()> {
1913 self.openhcl_diag()?.core_dump(name, path).await
1914 }
1915
1916 pub async fn openhcl_crash(&self, name: &str) -> anyhow::Result<()> {
1918 self.openhcl_diag()?.crash(name).await
1919 }
1920
1921 async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient> {
1924 self.runtime.wait_for_enlightened_shutdown_ready().await?;
1934 self.runtime.wait_for_agent(false).await
1935 }
1936
1937 pub async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient> {
1941 self.launch_vtl2_pipette().await?;
1943 self.runtime.wait_for_agent(true).await
1944 }
1945
1946 async fn wait_for_expected_boot_event(&mut self) -> anyhow::Result<()> {
1953 if let Some(expected_event) = self.expected_boot_event {
1954 let event = self.wait_for_boot_event().await?;
1955
1956 anyhow::ensure!(
1957 event == expected_event,
1958 "Did not receive expected boot event"
1959 );
1960 } else {
1961 tracing::warn!("Boot event not emitted for configured firmware or manually ignored.");
1962 }
1963
1964 Ok(())
1965 }
1966
1967 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
1970 tracing::info!("Waiting for boot event...");
1971 let boot_event = loop {
1972 match CancelContext::new()
1973 .with_timeout(self.vmm_quirks.flaky_boot.unwrap_or(Duration::MAX))
1974 .until_cancelled(self.runtime.wait_for_boot_event())
1975 .await
1976 {
1977 Ok(res) => break res?,
1978 Err(_) => {
1979 tracing::error!("Did not get boot event in required time, resetting...");
1980 if let Some(inspector) = self.runtime.inspector() {
1981 save_inspect(
1982 "vmm",
1983 Box::pin(async move { inspector.inspect("").await }),
1984 &self.resources.log_source,
1985 )
1986 .await;
1987 }
1988
1989 self.runtime.reset().await?;
1990 continue;
1991 }
1992 }
1993 };
1994 tracing::info!("Got boot event: {boot_event:?}");
1995 Ok(boot_event)
1996 }
1997
1998 pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
2001 tracing::info!("Waiting for enlightened shutdown to be ready");
2002 self.runtime.wait_for_enlightened_shutdown_ready().await?;
2003
2004 let mut wait_time = Duration::from_secs(10);
2010
2011 if let Some(duration) = self.guest_quirks.hyperv_shutdown_ic_sleep {
2013 wait_time += duration;
2014 }
2015
2016 tracing::info!(
2017 "Shutdown IC reported ready, waiting for an extra {}s",
2018 wait_time.as_secs()
2019 );
2020 PolledTimer::new(&self.resources.driver)
2021 .sleep(wait_time)
2022 .await;
2023
2024 tracing::info!("Sending enlightened shutdown command");
2025 self.runtime.send_enlightened_shutdown(kind).await
2026 }
2027
2028 pub async fn restart_openhcl(
2031 &mut self,
2032 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
2033 flags: OpenHclServicingFlags,
2034 ) -> anyhow::Result<()> {
2035 self.runtime
2036 .restart_openhcl(&new_openhcl.erase(), flags)
2037 .await
2038 }
2039
2040 pub async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()> {
2043 self.runtime.update_command_line(command_line).await
2044 }
2045
2046 pub async fn add_pcie_device(
2048 &mut self,
2049 port_name: String,
2050 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
2051 ) -> anyhow::Result<()> {
2052 self.runtime.add_pcie_device(port_name, resource).await
2053 }
2054
2055 pub async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
2057 self.runtime.remove_pcie_device(port_name).await
2058 }
2059
2060 pub async fn save_openhcl(
2063 &mut self,
2064 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
2065 flags: OpenHclServicingFlags,
2066 ) -> anyhow::Result<()> {
2067 self.runtime.save_openhcl(&new_openhcl.erase(), flags).await
2068 }
2069
2070 pub async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
2073 self.runtime.restore_openhcl().await
2074 }
2075
2076 pub fn arch(&self) -> MachineArch {
2078 self.arch
2079 }
2080
2081 pub fn backend(&mut self) -> &mut T::VmRuntime {
2083 &mut self.runtime
2084 }
2085
2086 async fn launch_vtl2_pipette(&self) -> anyhow::Result<()> {
2087 tracing::debug!("Launching VTL 2 pipette...");
2088
2089 let res = self
2091 .openhcl_diag()?
2092 .run_vtl2_command("sh", &["-c", "mkdir /cidata && mount LABEL=cidata /cidata"])
2093 .await?;
2094
2095 if !res.exit_status.success() {
2096 anyhow::bail!("Failed to mount VTL 2 pipette drive: {:?}", res);
2097 }
2098
2099 let res = self
2100 .openhcl_diag()?
2101 .run_detached_vtl2_command("sh", &["-c", "/cidata/pipette 2>&1 | logger &"])
2102 .await?;
2103
2104 if !res.success() {
2105 anyhow::bail!("Failed to spawn VTL 2 pipette: {:?}", res);
2106 }
2107
2108 Ok(())
2109 }
2110
2111 fn openhcl_diag(&self) -> anyhow::Result<&OpenHclDiagHandler> {
2112 if let Some(ohd) = self.openhcl_diag_handler.as_ref() {
2113 Ok(ohd)
2114 } else {
2115 anyhow::bail!("VM is not configured with OpenHCL")
2116 }
2117 }
2118
2119 pub async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2121 self.runtime.get_guest_state_file().await
2122 }
2123
2124 pub async fn modify_vtl2_settings(
2126 &mut self,
2127 f: impl FnOnce(&mut Vtl2Settings),
2128 ) -> anyhow::Result<()> {
2129 if self.openhcl_diag_handler.is_none() {
2130 panic!("Custom VTL 2 settings are only supported with OpenHCL");
2131 }
2132 f(self
2133 .config
2134 .vtl2_settings
2135 .get_or_insert_with(default_vtl2_settings));
2136 self.runtime
2137 .set_vtl2_settings(self.config.vtl2_settings.as_ref().unwrap())
2138 .await
2139 }
2140
2141 pub fn get_vmbus_storage_controllers(&self) -> &HashMap<Guid, VmbusStorageController> {
2143 &self.config.vmbus_storage_controllers
2144 }
2145
2146 pub async fn set_vmbus_drive(
2148 &mut self,
2149 drive: Drive,
2150 controller_id: &Guid,
2151 controller_location: Option<u32>,
2152 ) -> anyhow::Result<()> {
2153 let controller = self
2154 .config
2155 .vmbus_storage_controllers
2156 .get_mut(controller_id)
2157 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
2158
2159 let controller_location = controller.set_drive(controller_location, drive, true);
2160 let disk = controller.drives.get(&controller_location).unwrap();
2161
2162 self.runtime
2163 .set_vmbus_drive(disk, controller_id, controller_location)
2164 .await?;
2165
2166 Ok(())
2167 }
2168}
2169
2170#[async_trait]
2172pub trait PetriVmRuntime: Send + Sync + 'static {
2173 type VmInspector: PetriVmInspector;
2175 type VmFramebufferAccess: PetriVmFramebufferAccess;
2177
2178 async fn teardown(self) -> anyhow::Result<()>;
2180 async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReasonDetail>;
2183 async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>;
2185 fn openhcl_diag(&self) -> Option<OpenHclDiagHandler>;
2187 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent>;
2190 async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()>;
2193 async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
2195 async fn restart_openhcl(
2198 &mut self,
2199 new_openhcl: &ResolvedArtifact,
2200 flags: OpenHclServicingFlags,
2201 ) -> anyhow::Result<()>;
2202 async fn save_openhcl(
2206 &mut self,
2207 new_openhcl: &ResolvedArtifact,
2208 flags: OpenHclServicingFlags,
2209 ) -> anyhow::Result<()>;
2210 async fn restore_openhcl(&mut self) -> anyhow::Result<()>;
2213 async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()>;
2216 fn inspector(&self) -> Option<Self::VmInspector> {
2218 None
2219 }
2220 fn take_framebuffer_access(&mut self) -> Option<Self::VmFramebufferAccess> {
2223 None
2224 }
2225 async fn reset(&mut self) -> anyhow::Result<()>;
2227 async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2229 Ok(None)
2230 }
2231 async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()>;
2233 async fn set_vmbus_drive(
2235 &mut self,
2236 disk: &Drive,
2237 controller_id: &Guid,
2238 controller_location: u32,
2239 ) -> anyhow::Result<()>;
2240 async fn add_pcie_device(
2242 &mut self,
2243 port_name: String,
2244 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
2245 ) -> anyhow::Result<()> {
2246 let _ = (port_name, resource);
2247 anyhow::bail!("PCIe hotplug not supported by this backend")
2248 }
2249 async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
2251 let _ = port_name;
2252 anyhow::bail!("PCIe hotplug not supported by this backend")
2253 }
2254}
2255
2256#[async_trait]
2258pub trait PetriVmInspector: Send + Sync + 'static {
2259 async fn inspect(&self, path: &str) -> anyhow::Result<inspect::Node>;
2262}
2263
2264pub struct NoPetriVmInspector;
2266#[async_trait]
2267impl PetriVmInspector for NoPetriVmInspector {
2268 async fn inspect(&self, _path: &str) -> anyhow::Result<inspect::Node> {
2269 unreachable!()
2270 }
2271}
2272
2273pub struct VmScreenshotMeta {
2275 pub color: image::ExtendedColorType,
2277 pub width: u16,
2279 pub height: u16,
2281}
2282
2283#[async_trait]
2285pub trait PetriVmFramebufferAccess: Send + 'static {
2286 async fn screenshot(&mut self, image: &mut Vec<u8>)
2289 -> anyhow::Result<Option<VmScreenshotMeta>>;
2290}
2291
2292pub struct NoPetriVmFramebufferAccess;
2294#[async_trait]
2295impl PetriVmFramebufferAccess for NoPetriVmFramebufferAccess {
2296 async fn screenshot(
2297 &mut self,
2298 _image: &mut Vec<u8>,
2299 ) -> anyhow::Result<Option<VmScreenshotMeta>> {
2300 unreachable!()
2301 }
2302}
2303
2304#[derive(Debug)]
2306pub struct ProcessorTopology {
2307 pub vp_count: u32,
2309 pub enable_smt: Option<bool>,
2311 pub vps_per_socket: Option<u32>,
2313 pub apic_mode: Option<ApicMode>,
2315}
2316
2317impl Default for ProcessorTopology {
2318 fn default() -> Self {
2319 Self {
2320 vp_count: 2,
2321 enable_smt: None,
2322 vps_per_socket: None,
2323 apic_mode: None,
2324 }
2325 }
2326}
2327
2328impl ProcessorTopology {
2329 pub fn heavy() -> Self {
2331 Self {
2332 vp_count: 16,
2333 vps_per_socket: Some(8),
2334 ..Default::default()
2335 }
2336 }
2337
2338 pub fn very_heavy() -> Self {
2340 Self {
2341 vp_count: 32,
2342 vps_per_socket: Some(16),
2343 ..Default::default()
2344 }
2345 }
2346}
2347
2348#[derive(Debug, Clone, Copy)]
2350pub enum ApicMode {
2351 Xapic,
2353 X2apicSupported,
2355 X2apicEnabled,
2357}
2358
2359#[derive(Debug)]
2361pub struct MemoryConfig {
2362 pub startup_bytes: u64,
2365 pub dynamic_memory_range: Option<(u64, u64)>,
2369 pub numa_mem_sizes: Option<Vec<u64>>,
2372 pub private_memory: Option<bool>,
2391 pub transparent_hugepages: bool,
2403}
2404
2405impl Default for MemoryConfig {
2406 fn default() -> Self {
2407 Self {
2408 startup_bytes: 4 * 1024 * 1024 * 1024, dynamic_memory_range: None,
2410 numa_mem_sizes: None,
2411 private_memory: None,
2412 transparent_hugepages: true,
2413 }
2414 }
2415}
2416
2417#[derive(Debug)]
2419pub struct UefiConfig {
2420 pub secure_boot_enabled: bool,
2422 pub secure_boot_template: Option<SecureBootTemplate>,
2424 pub custom_uefi_json: Option<Vec<u8>>,
2426 pub disable_frontpage: bool,
2428 pub default_boot_always_attempt: bool,
2430 pub enable_vpci_boot: bool,
2432 pub force_dma_bounce: bool,
2434 pub efi_diagnostics_log_level: EfiDiagnosticsLogLevel,
2436 pub efi_diagnostics_rate_limit: Option<u32>,
2439}
2440
2441impl Default for UefiConfig {
2442 fn default() -> Self {
2443 Self {
2444 secure_boot_enabled: false,
2445 secure_boot_template: None,
2446 custom_uefi_json: None,
2447 disable_frontpage: true,
2448 default_boot_always_attempt: false,
2449 enable_vpci_boot: false,
2450 force_dma_bounce: false,
2451 efi_diagnostics_log_level: EfiDiagnosticsLogLevel::Default,
2452 efi_diagnostics_rate_limit: None,
2453 }
2454 }
2455}
2456
2457#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2462pub enum EfiDiagnosticsLogLevel {
2463 #[default]
2465 Default,
2466 Info,
2468 Full,
2470}
2471
2472#[derive(Debug, Clone)]
2474pub enum OpenvmmLogConfig {
2475 TestDefault,
2479 BuiltInDefault,
2482 Custom(BTreeMap<String, String>),
2492}
2493
2494#[derive(Debug)]
2496pub struct OpenHclConfig {
2497 pub vmbus_redirect: bool,
2499 pub custom_command_line: Option<String>,
2503 pub log_levels: OpenvmmLogConfig,
2507 pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
2510 pub vtl2_settings: Option<Vtl2Settings>,
2512}
2513
2514impl OpenHclConfig {
2515 pub fn command_line(&self) -> String {
2518 let mut cmdline = self.custom_command_line.clone();
2519
2520 append_cmdline(&mut cmdline, "OPENHCL_MANA_KEEP_ALIVE=host,privatepool");
2522
2523 match &self.log_levels {
2524 OpenvmmLogConfig::TestDefault => {
2525 let default_log_levels = {
2526 let openhcl_tracing = if let Ok(x) =
2528 std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG"))
2529 {
2530 format!("OPENVMM_LOG={x}")
2531 } else {
2532 "OPENVMM_LOG=debug".to_owned()
2533 };
2534 let openhcl_show_spans = if let Ok(x) = std::env::var("OPENVMM_SHOW_SPANS") {
2535 format!("OPENVMM_SHOW_SPANS={x}")
2536 } else {
2537 "OPENVMM_SHOW_SPANS=true".to_owned()
2538 };
2539 format!("{openhcl_tracing} {openhcl_show_spans}")
2540 };
2541 append_cmdline(&mut cmdline, &default_log_levels);
2542 }
2543 OpenvmmLogConfig::BuiltInDefault => {
2544 }
2546 OpenvmmLogConfig::Custom(levels) => {
2547 levels.iter().for_each(|(key, value)| {
2548 append_cmdline(&mut cmdline, format!("{key}={value}"));
2549 });
2550 }
2551 }
2552
2553 cmdline.unwrap_or_default()
2554 }
2555}
2556
2557impl Default for OpenHclConfig {
2558 fn default() -> Self {
2559 Self {
2560 vmbus_redirect: false,
2561 custom_command_line: None,
2562 log_levels: OpenvmmLogConfig::TestDefault,
2563 vtl2_base_address_type: None,
2564 vtl2_settings: None,
2565 }
2566 }
2567}
2568
2569#[derive(Debug)]
2571pub struct TpmConfig {
2572 pub no_persistent_secrets: bool,
2574 pub hardware_sealing_policy: PetriHardwareSealingPolicy,
2576}
2577
2578impl Default for TpmConfig {
2579 fn default() -> Self {
2580 Self {
2581 no_persistent_secrets: true,
2582 hardware_sealing_policy: PetriHardwareSealingPolicy::Default,
2583 }
2584 }
2585}
2586
2587#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2592pub enum PetriHardwareSealingPolicy {
2593 #[default]
2595 Default,
2596 HashPolicy,
2598 SignerPolicy,
2600}
2601
2602#[derive(Debug)]
2606pub enum Firmware {
2607 LinuxDirect {
2609 kernel: ResolvedArtifact,
2611 initrd: ResolvedArtifact,
2613 },
2614 OpenhclLinuxDirect {
2616 igvm_path: ResolvedArtifact,
2618 openhcl_config: OpenHclConfig,
2620 },
2621 Pcat {
2623 guest: PcatGuest,
2625 bios_firmware: ResolvedOptionalArtifact,
2627 svga_firmware: ResolvedOptionalArtifact,
2629 ide_controllers: [[Option<Drive>; 2]; 2],
2631 },
2632 OpenhclPcat {
2634 guest: PcatGuest,
2636 igvm_path: ResolvedArtifact,
2638 bios_firmware: ResolvedOptionalArtifact,
2640 svga_firmware: ResolvedOptionalArtifact,
2642 openhcl_config: OpenHclConfig,
2644 },
2645 Uefi {
2647 guest: UefiGuest,
2649 uefi_firmware: ResolvedArtifact,
2651 uefi_config: UefiConfig,
2653 },
2654 OpenhclUefi {
2656 guest: UefiGuest,
2658 isolation: Option<IsolationType>,
2660 igvm_path: ResolvedArtifact,
2662 uefi_config: UefiConfig,
2664 openhcl_config: OpenHclConfig,
2666 },
2667}
2668
2669#[derive(Debug, Copy, Clone, PartialEq, Eq)]
2671pub enum BootDeviceType {
2672 None,
2674 Ide,
2676 IdeViaScsi,
2678 IdeViaNvme,
2680 Scsi,
2682 ScsiViaScsi,
2684 ScsiViaNvme,
2686 Nvme,
2688 NvmeViaScsi,
2690 NvmeViaNvme,
2692 PcieNvme,
2694 PcieVirtioBlk,
2696}
2697
2698impl BootDeviceType {
2699 fn requires_vtl2(&self) -> bool {
2700 match self {
2701 BootDeviceType::None
2702 | BootDeviceType::Ide
2703 | BootDeviceType::Scsi
2704 | BootDeviceType::Nvme
2705 | BootDeviceType::PcieNvme
2706 | BootDeviceType::PcieVirtioBlk => false,
2707 BootDeviceType::IdeViaScsi
2708 | BootDeviceType::IdeViaNvme
2709 | BootDeviceType::ScsiViaScsi
2710 | BootDeviceType::ScsiViaNvme
2711 | BootDeviceType::NvmeViaScsi
2712 | BootDeviceType::NvmeViaNvme => true,
2713 }
2714 }
2715
2716 fn requires_vpci_boot(&self) -> bool {
2717 matches!(
2718 self,
2719 BootDeviceType::Nvme | BootDeviceType::NvmeViaScsi | BootDeviceType::NvmeViaNvme
2720 )
2721 }
2722
2723 fn requires_vmbus(&self) -> bool {
2724 match self {
2725 BootDeviceType::None
2726 | BootDeviceType::Ide
2727 | BootDeviceType::PcieNvme
2728 | BootDeviceType::PcieVirtioBlk => false,
2729 BootDeviceType::IdeViaScsi
2730 | BootDeviceType::IdeViaNvme
2731 | BootDeviceType::Scsi
2732 | BootDeviceType::ScsiViaScsi
2733 | BootDeviceType::ScsiViaNvme
2734 | BootDeviceType::Nvme
2735 | BootDeviceType::NvmeViaScsi
2736 | BootDeviceType::NvmeViaNvme => true,
2737 }
2738 }
2739}
2740
2741impl Firmware {
2742 pub fn linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2744 use petri_artifacts_vmm_test::artifacts::loadable::*;
2745 match arch {
2746 MachineArch::X86_64 => Firmware::LinuxDirect {
2747 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_X64).erase(),
2748 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
2749 },
2750 MachineArch::Aarch64 => Firmware::LinuxDirect {
2751 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_AARCH64).erase(),
2752 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_AARCH64).erase(),
2753 },
2754 }
2755 }
2756
2757 pub fn linux_direct_bzimage(resolver: &ArtifactResolver<'_>) -> Self {
2762 use petri_artifacts_vmm_test::artifacts::loadable::*;
2763 Firmware::LinuxDirect {
2764 kernel: resolver.require(LINUX_DIRECT_TEST_BZIMAGE_X64).erase(),
2765 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
2766 }
2767 }
2768
2769 pub fn openhcl_linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2771 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2772 match arch {
2773 MachineArch::X86_64 => Firmware::OpenhclLinuxDirect {
2774 igvm_path: resolver.require(LATEST_LINUX_DIRECT_TEST_X64).erase(),
2775 openhcl_config: Default::default(),
2776 },
2777 MachineArch::Aarch64 => todo!("Linux direct not yet supported on aarch64"),
2778 }
2779 }
2780
2781 pub fn pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2783 use petri_artifacts_vmm_test::artifacts::loadable::*;
2784 Firmware::Pcat {
2785 guest,
2786 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2787 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2788 ide_controllers: [[None, None], [None, None]],
2789 }
2790 }
2791
2792 pub fn openhcl_pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2794 use petri_artifacts_vmm_test::artifacts::loadable::*;
2795 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2796 Firmware::OpenhclPcat {
2797 guest,
2798 igvm_path: resolver.require(LATEST_STANDARD_X64).erase(),
2799 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2800 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2801 openhcl_config: OpenHclConfig {
2802 vmbus_redirect: true,
2804 ..Default::default()
2805 },
2806 }
2807 }
2808
2809 pub fn uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch, guest: UefiGuest) -> Self {
2811 use petri_artifacts_vmm_test::artifacts::loadable::*;
2812 let uefi_firmware = match arch {
2813 MachineArch::X86_64 => resolver.require(UEFI_FIRMWARE_X64).erase(),
2814 MachineArch::Aarch64 => resolver.require(UEFI_FIRMWARE_AARCH64).erase(),
2815 };
2816 Firmware::Uefi {
2817 guest,
2818 uefi_firmware,
2819 uefi_config: Default::default(),
2820 }
2821 }
2822
2823 pub fn openhcl_uefi(
2825 resolver: &ArtifactResolver<'_>,
2826 arch: MachineArch,
2827 guest: UefiGuest,
2828 isolation: Option<IsolationType>,
2829 ) -> Self {
2830 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2831 let igvm_path = match arch {
2832 MachineArch::X86_64 if isolation.is_some() => resolver.require(LATEST_CVM_X64).erase(),
2833 MachineArch::X86_64 => resolver.require(LATEST_STANDARD_X64).erase(),
2834 MachineArch::Aarch64 => resolver.require(LATEST_STANDARD_AARCH64).erase(),
2835 };
2836 Firmware::OpenhclUefi {
2837 guest,
2838 isolation,
2839 igvm_path,
2840 uefi_config: Default::default(),
2841 openhcl_config: Default::default(),
2842 }
2843 }
2844
2845 fn is_openhcl(&self) -> bool {
2846 match self {
2847 Firmware::OpenhclLinuxDirect { .. }
2848 | Firmware::OpenhclUefi { .. }
2849 | Firmware::OpenhclPcat { .. } => true,
2850 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => false,
2851 }
2852 }
2853
2854 fn isolation(&self) -> Option<IsolationType> {
2855 match self {
2856 Firmware::OpenhclUefi { isolation, .. } => *isolation,
2857 Firmware::LinuxDirect { .. }
2858 | Firmware::Pcat { .. }
2859 | Firmware::Uefi { .. }
2860 | Firmware::OpenhclLinuxDirect { .. }
2861 | Firmware::OpenhclPcat { .. } => None,
2862 }
2863 }
2864
2865 fn is_linux_direct(&self) -> bool {
2866 match self {
2867 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => true,
2868 Firmware::Pcat { .. }
2869 | Firmware::Uefi { .. }
2870 | Firmware::OpenhclUefi { .. }
2871 | Firmware::OpenhclPcat { .. } => false,
2872 }
2873 }
2874
2875 pub fn linux_direct_initrd(&self) -> Option<&Path> {
2877 match self {
2878 Firmware::LinuxDirect { initrd, .. } => Some(initrd.get()),
2879 _ => None,
2880 }
2881 }
2882
2883 fn is_pcat(&self) -> bool {
2884 match self {
2885 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => true,
2886 Firmware::Uefi { .. }
2887 | Firmware::OpenhclUefi { .. }
2888 | Firmware::LinuxDirect { .. }
2889 | Firmware::OpenhclLinuxDirect { .. } => false,
2890 }
2891 }
2892
2893 fn os_flavor(&self) -> OsFlavor {
2894 match self {
2895 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => OsFlavor::Linux,
2896 Firmware::Uefi {
2897 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2898 ..
2899 }
2900 | Firmware::OpenhclUefi {
2901 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2902 ..
2903 } => OsFlavor::Uefi,
2904 Firmware::Pcat {
2905 guest: PcatGuest::Vhd(cfg),
2906 ..
2907 }
2908 | Firmware::OpenhclPcat {
2909 guest: PcatGuest::Vhd(cfg),
2910 ..
2911 }
2912 | Firmware::Uefi {
2913 guest: UefiGuest::Vhd(cfg),
2914 ..
2915 }
2916 | Firmware::OpenhclUefi {
2917 guest: UefiGuest::Vhd(cfg),
2918 ..
2919 } => cfg.os_flavor,
2920 Firmware::Pcat {
2921 guest: PcatGuest::Iso(cfg),
2922 ..
2923 }
2924 | Firmware::OpenhclPcat {
2925 guest: PcatGuest::Iso(cfg),
2926 ..
2927 } => cfg.os_flavor,
2928 }
2929 }
2930
2931 fn quirks(&self) -> GuestQuirks {
2932 match self {
2933 Firmware::Pcat {
2934 guest: PcatGuest::Vhd(cfg),
2935 ..
2936 }
2937 | Firmware::Uefi {
2938 guest: UefiGuest::Vhd(cfg),
2939 ..
2940 }
2941 | Firmware::OpenhclUefi {
2942 guest: UefiGuest::Vhd(cfg),
2943 ..
2944 } => cfg.quirks.clone(),
2945 Firmware::Pcat {
2946 guest: PcatGuest::Iso(cfg),
2947 ..
2948 } => cfg.quirks.clone(),
2949 _ => Default::default(),
2950 }
2951 }
2952
2953 fn expected_boot_event(&self) -> Option<FirmwareEvent> {
2954 match self {
2955 Firmware::LinuxDirect { .. }
2956 | Firmware::OpenhclLinuxDirect { .. }
2957 | Firmware::Uefi {
2958 guest: UefiGuest::GuestTestUefi(_),
2959 ..
2960 }
2961 | Firmware::OpenhclUefi {
2962 guest: UefiGuest::GuestTestUefi(_),
2963 ..
2964 } => None,
2965 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => {
2966 Some(FirmwareEvent::BootAttempt)
2968 }
2969 Firmware::Uefi {
2970 guest: UefiGuest::None,
2971 ..
2972 }
2973 | Firmware::OpenhclUefi {
2974 guest: UefiGuest::None,
2975 ..
2976 } => Some(FirmwareEvent::NoBootDevice),
2977 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => {
2978 Some(FirmwareEvent::BootSuccess)
2979 }
2980 }
2981 }
2982
2983 fn openhcl_config(&self) -> Option<&OpenHclConfig> {
2984 match self {
2985 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2986 | Firmware::OpenhclUefi { openhcl_config, .. }
2987 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
2988 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2989 }
2990 }
2991
2992 fn openhcl_config_mut(&mut self) -> Option<&mut OpenHclConfig> {
2993 match self {
2994 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2995 | Firmware::OpenhclUefi { openhcl_config, .. }
2996 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
2997 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2998 }
2999 }
3000
3001 #[cfg_attr(not(windows), expect(dead_code))]
3002 fn openhcl_firmware(&self) -> Option<&Path> {
3003 match self {
3004 Firmware::OpenhclLinuxDirect { igvm_path, .. }
3005 | Firmware::OpenhclUefi { igvm_path, .. }
3006 | Firmware::OpenhclPcat { igvm_path, .. } => Some(igvm_path.get()),
3007 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
3008 }
3009 }
3010
3011 fn into_runtime_config(
3012 self,
3013 vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
3014 ) -> PetriVmRuntimeConfig {
3015 match self {
3016 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
3017 | Firmware::OpenhclUefi { openhcl_config, .. }
3018 | Firmware::OpenhclPcat { openhcl_config, .. } => PetriVmRuntimeConfig {
3019 vtl2_settings: Some(
3020 openhcl_config
3021 .vtl2_settings
3022 .unwrap_or_else(default_vtl2_settings),
3023 ),
3024 ide_controllers: None,
3025 vmbus_storage_controllers,
3026 },
3027 Firmware::Pcat {
3028 ide_controllers, ..
3029 } => PetriVmRuntimeConfig {
3030 vtl2_settings: None,
3031 ide_controllers: Some(ide_controllers),
3032 vmbus_storage_controllers,
3033 },
3034 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } => PetriVmRuntimeConfig {
3035 vtl2_settings: None,
3036 ide_controllers: None,
3037 vmbus_storage_controllers,
3038 },
3039 }
3040 }
3041
3042 fn uefi_config(&self) -> Option<&UefiConfig> {
3043 match self {
3044 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
3045 Some(uefi_config)
3046 }
3047 Firmware::LinuxDirect { .. }
3048 | Firmware::OpenhclLinuxDirect { .. }
3049 | Firmware::Pcat { .. }
3050 | Firmware::OpenhclPcat { .. } => None,
3051 }
3052 }
3053
3054 fn uefi_config_mut(&mut self) -> Option<&mut UefiConfig> {
3055 match self {
3056 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
3057 Some(uefi_config)
3058 }
3059 Firmware::LinuxDirect { .. }
3060 | Firmware::OpenhclLinuxDirect { .. }
3061 | Firmware::Pcat { .. }
3062 | Firmware::OpenhclPcat { .. } => None,
3063 }
3064 }
3065
3066 fn boot_drive(&self) -> Option<Drive> {
3067 match self {
3068 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => None,
3069 Firmware::Pcat { guest, .. } | Firmware::OpenhclPcat { guest, .. } => {
3070 Some((guest.disk_path(), guest.is_dvd()))
3071 }
3072 Firmware::Uefi { guest, .. } | Firmware::OpenhclUefi { guest, .. } => {
3073 guest.disk_path().map(|dp| (dp, false))
3074 }
3075 }
3076 .map(|(disk_path, is_dvd)| Drive::new(Some(Disk::Differencing(disk_path)), is_dvd))
3077 }
3078
3079 fn vtl2_settings(&mut self) -> Option<&mut Vtl2Settings> {
3080 self.openhcl_config_mut()
3081 .map(|c| c.vtl2_settings.get_or_insert_with(default_vtl2_settings))
3082 }
3083
3084 fn ide_controllers(&self) -> Option<&[[Option<Drive>; 2]; 2]> {
3085 match self {
3086 Firmware::Pcat {
3087 ide_controllers, ..
3088 } => Some(ide_controllers),
3089 _ => None,
3090 }
3091 }
3092
3093 fn ide_controllers_mut(&mut self) -> Option<&mut [[Option<Drive>; 2]; 2]> {
3094 match self {
3095 Firmware::Pcat {
3096 ide_controllers, ..
3097 } => Some(ide_controllers),
3098 _ => None,
3099 }
3100 }
3101}
3102
3103#[derive(Debug)]
3106pub enum PcatGuest {
3107 Vhd(BootImageConfig<boot_image_type::Vhd>),
3109 Iso(BootImageConfig<boot_image_type::Iso>),
3111}
3112
3113impl PcatGuest {
3114 fn disk_path(&self) -> DiskPath {
3115 match self {
3116 PcatGuest::Vhd(disk) => disk.disk_path(),
3117 PcatGuest::Iso(disk) => disk.disk_path(),
3118 }
3119 }
3120
3121 fn is_dvd(&self) -> bool {
3122 matches!(self, Self::Iso(_))
3123 }
3124}
3125
3126#[derive(Debug)]
3129pub enum UefiGuest {
3130 Vhd(BootImageConfig<boot_image_type::Vhd>),
3132 GuestTestUefi(ResolvedArtifact),
3134 None,
3136}
3137
3138impl UefiGuest {
3139 pub fn guest_test_uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
3141 use petri_artifacts_vmm_test::artifacts::test_vhd::*;
3142 let artifact = match arch {
3143 MachineArch::X86_64 => resolver.require(GUEST_TEST_UEFI_X64).erase(),
3144 MachineArch::Aarch64 => resolver.require(GUEST_TEST_UEFI_AARCH64).erase(),
3145 };
3146 UefiGuest::GuestTestUefi(artifact)
3147 }
3148
3149 fn disk_path(&self) -> Option<DiskPath> {
3150 match self {
3151 UefiGuest::Vhd(vhd) => Some(vhd.disk_path()),
3152 UefiGuest::GuestTestUefi(p) => Some(DiskPath::Local(p.get().to_path_buf())),
3153 UefiGuest::None => None,
3154 }
3155 }
3156}
3157
3158pub mod boot_image_type {
3160 mod private {
3161 pub trait Sealed {}
3162 impl Sealed for super::Vhd {}
3163 impl Sealed for super::Iso {}
3164 }
3165
3166 pub trait BootImageType: private::Sealed {}
3169
3170 #[derive(Debug)]
3172 pub enum Vhd {}
3173
3174 #[derive(Debug)]
3176 pub enum Iso {}
3177
3178 impl BootImageType for Vhd {}
3179 impl BootImageType for Iso {}
3180}
3181
3182#[derive(Debug)]
3184pub struct BootImageConfig<T: boot_image_type::BootImageType> {
3185 artifact: ResolvedArtifactSource,
3187 os_flavor: OsFlavor,
3189 quirks: GuestQuirks,
3193 _type: core::marker::PhantomData<T>,
3195}
3196
3197impl<T: boot_image_type::BootImageType> BootImageConfig<T> {
3198 fn disk_path(&self) -> DiskPath {
3200 match self.artifact.get() {
3201 ArtifactSource::Local(p) => DiskPath::Local(p.clone()),
3202 ArtifactSource::Remote { url } => DiskPath::Remote { url: url.clone() },
3203 }
3204 }
3205}
3206
3207impl BootImageConfig<boot_image_type::Vhd> {
3208 pub fn from_vhd<A>(artifact: ResolvedArtifactSource<A>) -> Self
3210 where
3211 A: petri_artifacts_common::tags::IsTestVhd,
3212 {
3213 BootImageConfig {
3214 artifact: artifact.erase(),
3215 os_flavor: A::OS_FLAVOR,
3216 quirks: A::quirks(),
3217 _type: std::marker::PhantomData,
3218 }
3219 }
3220}
3221
3222impl BootImageConfig<boot_image_type::Iso> {
3223 pub fn from_iso<A>(artifact: ResolvedArtifactSource<A>) -> Self
3225 where
3226 A: petri_artifacts_common::tags::IsTestIso,
3227 {
3228 BootImageConfig {
3229 artifact: artifact.erase(),
3230 os_flavor: A::OS_FLAVOR,
3231 quirks: A::quirks(),
3232 _type: std::marker::PhantomData,
3233 }
3234 }
3235}
3236
3237#[derive(Debug, Clone, Copy)]
3239pub enum IsolationType {
3240 Vbs,
3242 Snp,
3244 Tdx,
3246}
3247
3248#[derive(Debug, Clone, Copy)]
3250pub struct OpenHclServicingFlags {
3251 pub enable_nvme_keepalive: bool,
3254 pub enable_mana_keepalive: bool,
3256 pub override_version_checks: bool,
3258 pub stop_timeout_hint_secs: Option<u16>,
3260}
3261
3262#[derive(Debug, Clone)]
3264pub enum DiskPath {
3265 Local(PathBuf),
3267 Remote {
3269 url: String,
3271 },
3272}
3273
3274impl From<PathBuf> for DiskPath {
3275 fn from(path: PathBuf) -> Self {
3276 DiskPath::Local(path)
3277 }
3278}
3279
3280#[derive(Debug, Clone)]
3282pub enum Disk {
3283 Memory(u64),
3285 Differencing(DiskPath),
3287 Persistent(PathBuf),
3289 Temporary(Arc<TempPath>),
3291}
3292
3293#[derive(Debug, Clone)]
3295pub struct PetriVmgsDisk {
3296 pub disk: Disk,
3298 pub encryption_policy: GuestStateEncryptionPolicy,
3300}
3301
3302impl Default for PetriVmgsDisk {
3303 fn default() -> Self {
3304 PetriVmgsDisk {
3305 disk: Disk::Memory(vmgs_format::VMGS_DEFAULT_CAPACITY),
3306 encryption_policy: GuestStateEncryptionPolicy::None(false),
3308 }
3309 }
3310}
3311
3312#[derive(Debug, Clone)]
3314pub enum PetriVmgsResource {
3315 Disk(PetriVmgsDisk),
3317 ReprovisionOnFailure(PetriVmgsDisk),
3319 Reprovision(PetriVmgsDisk),
3321 Ephemeral,
3323}
3324
3325impl PetriVmgsResource {
3326 pub fn vmgs(&self) -> Option<&PetriVmgsDisk> {
3328 match self {
3329 PetriVmgsResource::Disk(vmgs)
3330 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
3331 | PetriVmgsResource::Reprovision(vmgs) => Some(vmgs),
3332 PetriVmgsResource::Ephemeral => None,
3333 }
3334 }
3335
3336 pub fn disk(&self) -> Option<&Disk> {
3338 self.vmgs().map(|vmgs| &vmgs.disk)
3339 }
3340
3341 pub fn encryption_policy(&self) -> Option<GuestStateEncryptionPolicy> {
3343 self.vmgs().map(|vmgs| vmgs.encryption_policy)
3344 }
3345}
3346
3347#[derive(Debug, Clone, Copy)]
3349pub enum PetriGuestStateLifetime {
3350 Disk,
3353 ReprovisionOnFailure,
3355 Reprovision,
3357 Ephemeral,
3359}
3360
3361#[derive(Debug, Clone, Copy)]
3363pub enum SecureBootTemplate {
3364 MicrosoftWindows,
3366 MicrosoftUefiCertificateAuthority,
3368}
3369
3370#[derive(Default, Debug, Clone)]
3373pub struct VmmQuirks {
3374 pub flaky_boot: Option<Duration>,
3377}
3378
3379fn make_vm_safe_name(name: &str) -> String {
3385 const MAX_VM_NAME_LENGTH: usize = 100;
3386 const HASH_LENGTH: usize = 4;
3387 const MAX_PREFIX_LENGTH: usize = MAX_VM_NAME_LENGTH - HASH_LENGTH;
3388
3389 if name.len() <= MAX_VM_NAME_LENGTH {
3390 name.to_owned()
3391 } else {
3392 let mut hasher = DefaultHasher::new();
3394 name.hash(&mut hasher);
3395 let hash = hasher.finish();
3396
3397 let hash_suffix = format!("{:04x}", hash & 0xFFFF);
3399
3400 let truncated = &name[..MAX_PREFIX_LENGTH];
3402 tracing::debug!(
3403 "VM name too long ({}), truncating '{}' to '{}{}'",
3404 name.len(),
3405 name,
3406 truncated,
3407 hash_suffix
3408 );
3409
3410 format!("{}{}", truncated, hash_suffix)
3411 }
3412}
3413
3414#[derive(Debug, Clone, Copy, Eq, PartialEq)]
3416pub enum PetriHaltReason {
3417 PowerOff,
3419 Reset,
3421 Hibernate,
3423 TripleFault,
3425 Other,
3427}
3428
3429impl PetriHaltReason {
3430 pub fn with_detail(self, detail: String) -> PetriHaltReasonDetail {
3432 PetriHaltReasonDetail {
3433 reason: self,
3434 detail,
3435 }
3436 }
3437}
3438
3439#[derive(Debug, Clone)]
3441pub struct PetriHaltReasonDetail {
3442 pub reason: PetriHaltReason,
3444 pub detail: String,
3446}
3447
3448fn append_cmdline(cmd: &mut Option<String>, add_cmd: impl AsRef<str>) {
3449 if let Some(cmd) = cmd.as_mut() {
3450 cmd.push(' ');
3451 cmd.push_str(add_cmd.as_ref());
3452 } else {
3453 *cmd = Some(add_cmd.as_ref().to_string());
3454 }
3455}
3456
3457async fn save_inspect(
3458 name: &str,
3459 inspect: std::pin::Pin<Box<dyn Future<Output = anyhow::Result<inspect::Node>> + Send>>,
3460 log_source: &PetriLogSource,
3461) {
3462 tracing::info!("Collecting {name} inspect details.");
3463 let node = match inspect.await {
3464 Ok(n) => n,
3465 Err(e) => {
3466 tracing::error!(?e, "Failed to get {name}");
3467 return;
3468 }
3469 };
3470 if let Err(e) = log_source.write_attachment(
3471 &format!("timeout_inspect_{name}.log"),
3472 format!("{node:#}").as_bytes(),
3473 ) {
3474 tracing::error!(?e, "Failed to save {name} inspect log");
3475 return;
3476 }
3477 tracing::info!("{name} inspect task finished.");
3478}
3479
3480pub struct ModifyFn<T>(pub Box<dyn FnOnce(T) -> T + Send>);
3482
3483impl<T> Debug for ModifyFn<T> {
3484 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3485 write!(f, "_")
3486 }
3487}
3488
3489fn default_vtl2_settings() -> Vtl2Settings {
3491 Vtl2Settings {
3492 version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
3493 fixed: None,
3494 dynamic: Some(Default::default()),
3495 namespace_settings: Default::default(),
3496 }
3497}
3498
3499#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3501pub enum Vtl {
3502 Vtl0 = 0,
3504 Vtl1 = 1,
3506 Vtl2 = 2,
3508}
3509
3510#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3512pub enum VmbusStorageType {
3513 Scsi,
3515 Nvme,
3517 VirtioBlk,
3519}
3520
3521#[derive(Debug, Clone)]
3523pub struct Drive {
3524 pub disk: Option<Disk>,
3526 pub is_dvd: bool,
3528}
3529
3530impl Drive {
3531 pub fn new(disk: Option<Disk>, is_dvd: bool) -> Self {
3533 Self { disk, is_dvd }
3534 }
3535}
3536
3537#[derive(Debug, Clone)]
3539pub struct VmbusStorageController {
3540 pub target_vtl: Vtl,
3542 pub controller_type: VmbusStorageType,
3544 pub drives: HashMap<u32, Drive>,
3546}
3547
3548impl VmbusStorageController {
3549 pub fn new(target_vtl: Vtl, controller_type: VmbusStorageType) -> Self {
3551 Self {
3552 target_vtl,
3553 controller_type,
3554 drives: HashMap::new(),
3555 }
3556 }
3557
3558 pub fn set_drive(
3560 &mut self,
3561 lun: Option<u32>,
3562 drive: Drive,
3563 allow_modify_existing: bool,
3564 ) -> u32 {
3565 let lun = lun.unwrap_or_else(|| {
3566 let mut lun = None;
3568 for x in 0..u8::MAX as u32 {
3569 if !self.drives.contains_key(&x) {
3570 lun = Some(x);
3571 break;
3572 }
3573 }
3574 lun.expect("all locations on this controller are in use")
3575 });
3576
3577 if self.drives.insert(lun, drive).is_some() && !allow_modify_existing {
3578 panic!("a disk with lun {lun} already existed on this controller");
3579 }
3580
3581 lun
3582 }
3583}
3584
3585pub(crate) fn petri_disk_cache_dir() -> String {
3587 if let Ok(dir) = std::env::var("PETRI_CACHE_DIR") {
3588 return dir;
3589 }
3590
3591 #[cfg(target_os = "macos")]
3592 {
3593 if let Ok(home) = std::env::var("HOME") {
3594 return format!("{home}/Library/Caches/petri");
3595 }
3596 }
3597
3598 #[cfg(windows)]
3599 {
3600 if let Ok(local) = std::env::var("LOCALAPPDATA") {
3601 return format!("{local}\\petri\\cache");
3602 }
3603 }
3604
3605 if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
3607 return format!("{xdg}/petri");
3608 }
3609 if let Ok(home) = std::env::var("HOME") {
3610 return format!("{home}/.cache/petri");
3611 }
3612
3613 ".cache/petri".to_string()
3614}
3615
3616#[cfg(test)]
3617mod tests {
3618 use super::make_vm_safe_name;
3619 use crate::Drive;
3620 use crate::VmbusStorageController;
3621 use crate::VmbusStorageType;
3622 use crate::Vtl;
3623
3624 #[test]
3625 fn test_short_names_unchanged() {
3626 let short_name = "short_test_name";
3627 assert_eq!(make_vm_safe_name(short_name), short_name);
3628 }
3629
3630 #[test]
3631 fn test_exactly_100_chars_unchanged() {
3632 let name_100 = "a".repeat(100);
3633 assert_eq!(make_vm_safe_name(&name_100), name_100);
3634 }
3635
3636 #[test]
3637 fn test_long_name_truncated() {
3638 let long_name = "multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_openhcl_servicing";
3639 let result = make_vm_safe_name(long_name);
3640
3641 assert_eq!(result.len(), 100);
3643
3644 assert!(result.starts_with("multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_ope"));
3646
3647 let suffix = &result[96..];
3649 assert_eq!(suffix.len(), 4);
3650 assert!(u16::from_str_radix(suffix, 16).is_ok());
3652 }
3653
3654 #[test]
3655 fn test_deterministic_results() {
3656 let long_name = "very_long_test_name_that_exceeds_the_100_character_limit_and_should_be_truncated_consistently_every_time";
3657 let result1 = make_vm_safe_name(long_name);
3658 let result2 = make_vm_safe_name(long_name);
3659
3660 assert_eq!(result1, result2);
3661 assert_eq!(result1.len(), 100);
3662 }
3663
3664 #[test]
3665 fn test_different_names_different_hashes() {
3666 let name1 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_1";
3667 let name2 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_2";
3668
3669 let result1 = make_vm_safe_name(name1);
3670 let result2 = make_vm_safe_name(name2);
3671
3672 assert_eq!(result1.len(), 100);
3674 assert_eq!(result2.len(), 100);
3675
3676 assert_ne!(result1, result2);
3678 assert_ne!(&result1[96..], &result2[96..]);
3679 }
3680
3681 #[test]
3682 fn test_vmbus_storage_controller() {
3683 let mut controller = VmbusStorageController::new(Vtl::Vtl0, VmbusStorageType::Scsi);
3684 assert_eq!(
3685 controller.set_drive(Some(1), Drive::new(None, false), false),
3686 1
3687 );
3688 assert!(controller.drives.contains_key(&1));
3689 assert_eq!(
3690 controller.set_drive(None, Drive::new(None, false), false),
3691 0
3692 );
3693 assert!(controller.drives.contains_key(&0));
3694 assert_eq!(
3695 controller.set_drive(None, Drive::new(None, false), false),
3696 2
3697 );
3698 assert!(controller.drives.contains_key(&2));
3699 assert_eq!(
3700 controller.set_drive(Some(0), Drive::new(None, false), true),
3701 0
3702 );
3703 assert!(controller.drives.contains_key(&0));
3704 }
3705}