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 no_hv: bool,
195}
196
197impl<T: PetriVmmBackend> Debug for PetriVmBuilder<T> {
198 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199 f.debug_struct("PetriVmBuilder")
200 .field("backend", &self.backend)
201 .field("config", &self.config)
202 .field("modify_vmm_config", &self.modify_vmm_config.is_some())
203 .field("resources", &self.resources)
204 .field("guest_quirks", &self.guest_quirks)
205 .field("vmm_quirks", &self.vmm_quirks)
206 .field("expected_boot_event", &self.expected_boot_event)
207 .field("override_expect_reset", &self.override_expect_reset)
208 .field("agent_image", &self.agent_image)
209 .field("openhcl_agent_image", &self.openhcl_agent_image)
210 .field("boot_device_type", &self.boot_device_type)
211 .field("pcie_boot_port", &self.pcie_boot_port)
212 .field("minimal_mode", &self.minimal_mode)
213 .field("enable_serial", &self.enable_serial)
214 .field("enable_screenshots", &self.enable_screenshots)
215 .field("prebuilt_initrd", &self.prebuilt_initrd)
216 .field("use_virtio_vsock", &self.use_virtio_vsock)
217 .field("no_vmbus", &self.no_vmbus)
218 .field("no_hv", &self.no_hv)
219 .finish()
220 }
221}
222
223#[derive(Debug)]
225pub struct PetriVmConfig {
226 pub name: String,
228 pub arch: MachineArch,
230 pub host_log_levels: Option<OpenvmmLogConfig>,
232 pub firmware: Firmware,
234 pub hibernation_enabled: bool,
236 pub ipmi_enabled: bool,
238 pub memory: MemoryConfig,
240 pub proc_topology: ProcessorTopology,
242 pub vmgs: PetriVmgsResource,
244 pub tpm: Option<TpmConfig>,
246 pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
248 pub pcie_nvme_drives: Vec<PcieNvmeDrive>,
250 pub pcie_virtio_blk_drives: Vec<PcieVirtioBlkDrive>,
252 pub physical_nvme_devices: HashMap<Guid, PhysicalNvmeDevice>,
254}
255
256#[derive(Debug)]
258pub struct PcieNvmeDrive {
259 pub port_name: String,
261 pub nsid: u32,
263 pub drive: Drive,
265}
266
267#[derive(Debug)]
269pub struct PcieVirtioBlkDrive {
270 pub port_name: String,
272 pub drive: Drive,
274}
275
276#[derive(Debug, Clone)]
279pub struct PhysicalNvmeDevice {
280 pub target_vtl: Vtl,
282 pub nsid: u32,
284 pub namespace_size_mib: u64,
286}
287
288pub struct PetriVmProperties {
291 pub is_openhcl: bool,
293 pub is_isolated: bool,
295 pub is_pcat: bool,
297 pub is_linux_direct: bool,
299 pub using_vtl0_pipette: bool,
301 pub using_vpci: bool,
303 pub os_flavor: OsFlavor,
305 pub minimal_mode: bool,
307 pub uses_pipette_as_init: bool,
309 pub enable_serial: bool,
311 pub prebuilt_initrd: Option<PathBuf>,
313 pub has_agent_disk: bool,
315 pub use_virtio_vsock: bool,
317 #[cfg(target_os = "linux")]
319 pub vhost_vsock_guest_cid: Option<u32>,
320 pub no_vmbus: bool,
322 pub no_hv: bool,
324}
325
326pub struct PetriVmRuntimeConfig {
328 pub vtl2_settings: Option<Vtl2Settings>,
330 pub ide_controllers: Option<[[Option<Drive>; 2]; 2]>,
332 pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
334}
335
336#[derive(Debug)]
338pub struct PetriVmResources {
339 driver: DefaultDriver,
340 log_source: PetriLogSource,
341}
342
343#[async_trait]
345pub trait PetriVmmBackend: Debug {
346 type VmmConfig;
348
349 type VmRuntime: PetriVmRuntime;
351
352 fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool;
355
356 fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks);
358
359 fn default_servicing_flags() -> OpenHclServicingFlags;
361
362 fn create_guest_dump_disk() -> anyhow::Result<
365 Option<(
366 Arc<TempPath>,
367 Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
368 )>,
369 >;
370
371 fn new(resolver: &ArtifactResolver<'_>) -> Self;
373
374 async fn run(
376 self,
377 config: PetriVmConfig,
378 modify_vmm_config: Option<ModifyFn<Self::VmmConfig>>,
379 resources: &PetriVmResources,
380 properties: PetriVmProperties,
381 ) -> anyhow::Result<(Self::VmRuntime, PetriVmRuntimeConfig)>;
382}
383
384pub(crate) const PETRI_IDE_BOOT_CONTROLLER_NUMBER: u32 = 0;
386pub(crate) const PETRI_IDE_BOOT_LUN: u8 = 0;
387pub(crate) const PETRI_IDE_BOOT_CONTROLLER: Guid =
388 guid::guid!("ca56751f-e643-4bef-bf54-f73678e8b7b5");
389
390pub(crate) const PETRI_SCSI_BOOT_LUN: u32 = 0;
392pub(crate) const PETRI_SCSI_PIPETTE_LUN: u32 = 1;
393pub(crate) const PETRI_SCSI_CRASH_LUN: u32 = 2;
394pub(crate) const PETRI_SCSI_VTL0_CONTROLLER: Guid =
396 guid::guid!("27b553e8-8b39-411b-a55f-839971a7884f");
397pub(crate) const PETRI_SCSI_VTL2_CONTROLLER: Guid =
399 guid::guid!("766e96f8-2ceb-437e-afe3-a93169e48a7c");
400pub(crate) const PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER: Guid =
402 guid::guid!("6c474f47-ed39-49e6-bbb9-142177a1da6e");
403
404pub(crate) const PETRI_NVME_BOOT_NSID: u32 = 37;
406pub(crate) const PETRI_NVME_BOOT_VTL0_CONTROLLER: Guid =
408 guid::guid!("e23a04e2-90f5-4852-bc9d-e7ac691b756c");
409pub(crate) const PETRI_NVME_BOOT_VTL2_CONTROLLER: Guid =
411 guid::guid!("92bc8346-718b-449a-8751-edbf3dcd27e4");
412
413pub(crate) const PETRI_PCIE_NVME_AGENT_PORT: &str = "s0rc0rp1";
415pub(crate) const PETRI_PCIE_NVME_AGENT_NSID: u32 = 1;
417
418pub struct PetriVm<T: PetriVmmBackend> {
420 resources: PetriVmResources,
421 runtime: T::VmRuntime,
422 watchdog_tasks: Vec<Task<()>>,
423 openhcl_diag_handler: Option<OpenHclDiagHandler>,
424
425 arch: MachineArch,
426 guest_quirks: GuestQuirksInner,
427 vmm_quirks: VmmQuirks,
428 expected_boot_event: Option<FirmwareEvent>,
429
430 config: PetriVmRuntimeConfig,
431}
432
433impl<T: PetriVmmBackend> PetriVmBuilder<T> {
434 pub fn new(
436 params: PetriTestParams<'_>,
437 artifacts: PetriVmArtifacts<T>,
438 driver: &DefaultDriver,
439 ) -> anyhow::Result<Self> {
440 let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
441 let expected_boot_event = artifacts.firmware.expected_boot_event();
442 let boot_device_type = match artifacts.firmware {
443 Firmware::LinuxDirect { .. } => BootDeviceType::None,
444 Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
445 Firmware::Pcat { .. } => BootDeviceType::Ide,
446 Firmware::OpenhclPcat { .. } => BootDeviceType::IdeViaScsi,
447 Firmware::Uefi {
448 guest: UefiGuest::None,
449 ..
450 }
451 | Firmware::OpenhclUefi {
452 guest: UefiGuest::None,
453 ..
454 } => BootDeviceType::None,
455 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
456 };
457
458 Ok(Self {
459 backend: artifacts.backend,
460 config: PetriVmConfig {
461 name: make_vm_safe_name(params.test_name),
462 arch: artifacts.arch,
463 host_log_levels: None,
464 firmware: artifacts.firmware,
465 hibernation_enabled: false,
466 ipmi_enabled: false,
467 memory: Default::default(),
468 proc_topology: Default::default(),
469
470 vmgs: PetriVmgsResource::Ephemeral,
471 tpm: None,
472 vmbus_storage_controllers: HashMap::new(),
473 pcie_nvme_drives: Vec::new(),
474 pcie_virtio_blk_drives: Vec::new(),
475 physical_nvme_devices: HashMap::new(),
476 },
477 modify_vmm_config: None,
478 resources: PetriVmResources {
479 driver: driver.clone(),
480 log_source: params.logger.clone(),
481 },
482
483 guest_quirks,
484 vmm_quirks,
485 expected_boot_event,
486 override_expect_reset: false,
487
488 agent_image: artifacts.agent_image,
489 openhcl_agent_image: artifacts.openhcl_agent_image,
490 boot_device_type,
491 pcie_boot_port: None,
492
493 minimal_mode: false,
494 pipette_binary: artifacts.pipette_binary,
495 enable_serial: true,
496 enable_screenshots: true,
497 prebuilt_initrd: None,
498 use_virtio_vsock: false,
499 #[cfg(target_os = "linux")]
500 vhost_vsock_guest_cid: None,
501 no_vmbus: false,
502 no_hv: false,
503 }
504 .add_petri_scsi_controllers()
505 .add_guest_crash_disk(params.post_test_hooks))
506 }
507
508 pub fn minimal(
519 params: PetriTestParams<'_>,
520 artifacts: PetriVmArtifacts<T>,
521 driver: &DefaultDriver,
522 ) -> anyhow::Result<Self> {
523 let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
524 let expected_boot_event = artifacts.firmware.expected_boot_event();
525 let boot_device_type = match artifacts.firmware {
526 Firmware::LinuxDirect { .. } => BootDeviceType::None,
527 Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
528 Firmware::Pcat { .. } => BootDeviceType::Ide,
529 Firmware::OpenhclPcat { .. } => BootDeviceType::IdeViaScsi,
530 Firmware::Uefi {
531 guest: UefiGuest::None,
532 ..
533 }
534 | Firmware::OpenhclUefi {
535 guest: UefiGuest::None,
536 ..
537 } => BootDeviceType::None,
538 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
539 };
540
541 Ok(Self {
542 backend: artifacts.backend,
543 config: PetriVmConfig {
544 name: make_vm_safe_name(params.test_name),
545 arch: artifacts.arch,
546 host_log_levels: None,
547 firmware: artifacts.firmware,
548 hibernation_enabled: false,
549 ipmi_enabled: false,
550 memory: Default::default(),
551 proc_topology: Default::default(),
552
553 vmgs: PetriVmgsResource::Ephemeral,
554 tpm: None,
555 vmbus_storage_controllers: HashMap::new(),
556 pcie_nvme_drives: Vec::new(),
557 pcie_virtio_blk_drives: Vec::new(),
558 physical_nvme_devices: HashMap::new(),
559 },
560 modify_vmm_config: None,
561 resources: PetriVmResources {
562 driver: driver.clone(),
563 log_source: params.logger.clone(),
564 },
565
566 guest_quirks,
567 vmm_quirks,
568 expected_boot_event,
569 override_expect_reset: false,
570
571 agent_image: artifacts.agent_image,
572 openhcl_agent_image: artifacts.openhcl_agent_image,
573 boot_device_type,
574 pcie_boot_port: None,
575
576 minimal_mode: true,
577 pipette_binary: artifacts.pipette_binary,
578 enable_serial: false,
579 enable_screenshots: true,
580 prebuilt_initrd: None,
581 use_virtio_vsock: false,
582 #[cfg(target_os = "linux")]
583 vhost_vsock_guest_cid: None,
584 no_vmbus: false,
585 no_hv: false,
586 })
587 }
588
589 pub fn is_minimal(&self) -> bool {
591 self.minimal_mode
592 }
593
594 pub fn with_prebuilt_initrd(mut self, path: PathBuf) -> Self {
601 self.prebuilt_initrd = Some(path);
602 self
603 }
604
605 pub fn prepare_initrd(&self) -> anyhow::Result<TempPath> {
616 use anyhow::Context;
617 use std::io::Write;
618
619 let initrd_path = self
620 .config
621 .firmware
622 .linux_direct_initrd()
623 .context("prepare_initrd requires Linux direct boot with initrd")?;
624 let pipette_path = self
625 .pipette_binary
626 .as_ref()
627 .context("prepare_initrd requires a pipette binary")?;
628
629 let initrd_gz = std::fs::read(initrd_path)
630 .with_context(|| format!("failed to read initrd at {}", initrd_path.display()))?;
631 let pipette_data = std::fs::read(pipette_path.get()).with_context(|| {
632 format!(
633 "failed to read pipette binary at {}",
634 pipette_path.get().display()
635 )
636 })?;
637
638 let merged_gz =
639 initrd_cpio::inject_into_initrd(&initrd_gz, "pipette", &pipette_data, 0o100755)
640 .context("failed to inject pipette into initrd")?;
641
642 let mut tmp = tempfile::NamedTempFile::new()
643 .context("failed to create temp file for pre-built initrd")?;
644 tmp.write_all(&merged_gz)
645 .context("failed to write pre-built initrd")?;
646
647 Ok(tmp.into_temp_path())
648 }
649
650 pub fn with_serial_output(mut self) -> Self {
659 self.enable_serial = true;
660 self
661 }
662
663 pub fn without_serial_output(mut self) -> Self {
668 self.enable_serial = false;
669 self
670 }
671
672 pub fn without_screenshots(mut self) -> Self {
677 self.enable_screenshots = false;
678 self
679 }
680
681 pub fn with_virtio_vsock(mut self) -> Self {
692 self.use_virtio_vsock = true;
693 #[cfg(target_os = "linux")]
694 {
695 self.vhost_vsock_guest_cid = None;
696 }
697 self
698 }
699
700 #[cfg(target_os = "linux")]
706 pub fn with_vhost_vsock(mut self, guest_cid: u32) -> Self {
707 assert!(
708 (3..u32::MAX).contains(&guest_cid),
709 "vhost-vsock guest CID must be between 3 and {}",
710 u32::MAX - 1
711 );
712 self.use_virtio_vsock = true;
713 self.vhost_vsock_guest_cid = Some(guest_cid);
714 self
715 }
716
717 pub fn with_no_vmbus(mut self) -> Self {
725 self.no_vmbus = true;
726 if self.config.firmware.os_flavor() != OsFlavor::Windows {
727 self.use_virtio_vsock = true;
728 }
729 self.config.vmbus_storage_controllers.clear();
730 self
731 }
732
733 pub fn with_no_hv(mut self) -> Self {
739 self.no_hv = true;
740 self.with_no_vmbus()
741 }
742
743 fn add_petri_scsi_controllers(self) -> Self {
744 let builder = self.add_vmbus_storage_controller(
745 &PETRI_SCSI_VTL0_CONTROLLER,
746 Vtl::Vtl0,
747 VmbusStorageType::Scsi,
748 );
749
750 if builder.is_openhcl() {
751 builder.add_vmbus_storage_controller(
752 &PETRI_SCSI_VTL2_CONTROLLER,
753 Vtl::Vtl2,
754 VmbusStorageType::Scsi,
755 )
756 } else {
757 builder
758 }
759 }
760
761 fn add_guest_crash_disk(self, post_test_hooks: &mut Vec<PetriPostTestHook>) -> Self {
762 let logger = self.resources.log_source.clone();
763 let (disk, disk_hook) = matches!(
764 self.config.firmware.os_flavor(),
765 OsFlavor::Windows | OsFlavor::Linux
766 )
767 .then(|| T::create_guest_dump_disk().expect("failed to create guest dump disk"))
768 .flatten()
769 .unzip();
770
771 if let Some(disk_hook) = disk_hook {
772 post_test_hooks.push(PetriPostTestHook::new(
773 "extract guest crash dumps".into(),
774 move |test_passed| {
775 if test_passed {
776 return Ok(());
777 }
778 let mut disk = disk_hook()?;
779 let gpt = gptman::GPT::read_from(&mut disk, SECTOR_SIZE)?;
780 let partition = fscommon::StreamSlice::new(
781 &mut disk,
782 gpt[1].starting_lba * SECTOR_SIZE,
783 gpt[1].ending_lba * SECTOR_SIZE,
784 )?;
785 let fs = fatfs::FileSystem::new(partition, fatfs::FsOptions::new())?;
786 for entry in fs.root_dir().iter() {
787 let Ok(entry) = entry else {
788 tracing::warn!(?entry, "failed to read entry in guest crash dump disk");
789 continue;
790 };
791 if !entry.is_file() {
792 tracing::warn!(
793 ?entry,
794 "skipping non-file entry in guest crash dump disk"
795 );
796 continue;
797 }
798 logger.write_attachment(&entry.file_name(), entry.to_file())?;
799 }
800 Ok(())
801 },
802 ));
803 }
804
805 if let Some(disk) = disk {
806 self.add_vmbus_drive(
807 Drive::new(Some(Disk::Temporary(disk)), false),
808 &PETRI_SCSI_VTL0_CONTROLLER,
809 Some(PETRI_SCSI_CRASH_LUN),
810 )
811 } else {
812 self
813 }
814 }
815
816 fn add_agent_disks(self) -> Self {
817 self.add_agent_disk_inner(Vtl::Vtl0)
818 .add_agent_disk_inner(Vtl::Vtl2)
819 }
820
821 fn add_agent_disk_inner(mut self, target_vtl: Vtl) -> Self {
822 let (agent_image, controller_id) = match target_vtl {
823 Vtl::Vtl0 => (self.agent_image.as_ref(), PETRI_SCSI_VTL0_CONTROLLER),
824 Vtl::Vtl1 => panic!("no VTL1 agent disk"),
825 Vtl::Vtl2 => (
826 self.openhcl_agent_image.as_ref(),
827 PETRI_SCSI_VTL2_CONTROLLER,
828 ),
829 };
830
831 if target_vtl == Vtl::Vtl0
834 && self.uses_pipette_as_init()
835 && !agent_image.is_some_and(|i| i.has_extras())
836 {
837 return self;
838 }
839
840 let Some(agent_disk) = agent_image.and_then(|i| {
841 i.build(crate::disk_image::ImageType::Vhd)
842 .expect("failed to build agent image")
843 }) else {
844 return self;
845 };
846
847 if self.no_vmbus {
850 self.config.pcie_nvme_drives.push(PcieNvmeDrive {
851 port_name: PETRI_PCIE_NVME_AGENT_PORT.into(),
852 nsid: PETRI_PCIE_NVME_AGENT_NSID,
853 drive: Drive::new(
854 Some(Disk::Temporary(Arc::new(agent_disk.into_temp_path()))),
855 false,
856 ),
857 });
858 return self;
859 }
860
861 if !self
864 .config
865 .vmbus_storage_controllers
866 .contains_key(&controller_id)
867 {
868 self = self.add_vmbus_storage_controller(
869 &controller_id,
870 target_vtl,
871 VmbusStorageType::Scsi,
872 );
873 }
874
875 self.add_vmbus_drive(
876 Drive::new(
877 Some(Disk::Temporary(Arc::new(agent_disk.into_temp_path()))),
878 false,
879 ),
880 &controller_id,
881 Some(PETRI_SCSI_PIPETTE_LUN),
882 )
883 }
884
885 fn add_boot_disk(mut self) -> Self {
886 if self.boot_device_type.requires_vtl2() && !self.is_openhcl() {
887 panic!("boot device type {:?} requires vtl2", self.boot_device_type);
888 }
889
890 if self.no_vmbus && self.boot_device_type.requires_vmbus() {
891 panic!(
892 "boot device type {:?} requires vmbus, but vmbus is disabled; \
893 use with_boot_device_type(BootDeviceType::PcieNvme) or similar",
894 self.boot_device_type
895 );
896 }
897
898 if self.boot_device_type.requires_vpci_boot() {
899 self.config
900 .firmware
901 .uefi_config_mut()
902 .expect("vpci boot requires uefi")
903 .enable_vpci_boot = true;
904 }
905
906 if let Some(boot_drive) = self.config.firmware.boot_drive() {
907 match self.boot_device_type {
908 BootDeviceType::None => unreachable!(),
909 BootDeviceType::Ide => self.add_ide_drive(
910 boot_drive,
911 PETRI_IDE_BOOT_CONTROLLER_NUMBER,
912 PETRI_IDE_BOOT_LUN,
913 ),
914 BootDeviceType::IdeViaScsi => self
915 .add_vmbus_drive(
916 boot_drive,
917 &PETRI_SCSI_VTL2_CONTROLLER,
918 Some(PETRI_SCSI_BOOT_LUN),
919 )
920 .add_vtl2_storage_controller(
921 Vtl2StorageControllerBuilder::new(ControllerType::Ide)
922 .with_instance_id(PETRI_IDE_BOOT_CONTROLLER)
923 .add_lun(
924 Vtl2LunBuilder::disk()
925 .with_channel(PETRI_IDE_BOOT_CONTROLLER_NUMBER)
926 .with_location(PETRI_IDE_BOOT_LUN as u32)
927 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
928 ControllerType::Scsi,
929 PETRI_SCSI_VTL2_CONTROLLER,
930 PETRI_SCSI_BOOT_LUN,
931 )),
932 )
933 .build(),
934 ),
935 BootDeviceType::IdeViaNvme => todo!(),
936 BootDeviceType::Scsi => self.add_vmbus_drive(
937 boot_drive,
938 &PETRI_SCSI_VTL0_CONTROLLER,
939 Some(PETRI_SCSI_BOOT_LUN),
940 ),
941 BootDeviceType::ScsiViaScsi => self
942 .add_vmbus_drive(
943 boot_drive,
944 &PETRI_SCSI_VTL2_CONTROLLER,
945 Some(PETRI_SCSI_BOOT_LUN),
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::Scsi,
955 PETRI_SCSI_VTL2_CONTROLLER,
956 PETRI_SCSI_BOOT_LUN,
957 )),
958 )
959 .build(),
960 ),
961 BootDeviceType::ScsiViaNvme => self
962 .add_vmbus_storage_controller(
963 &PETRI_NVME_BOOT_VTL2_CONTROLLER,
964 Vtl::Vtl2,
965 VmbusStorageType::Nvme,
966 )
967 .add_vmbus_drive(
968 boot_drive,
969 &PETRI_NVME_BOOT_VTL2_CONTROLLER,
970 Some(PETRI_NVME_BOOT_NSID),
971 )
972 .add_vtl2_storage_controller(
973 Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
974 .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
975 .add_lun(
976 Vtl2LunBuilder::disk()
977 .with_location(PETRI_SCSI_BOOT_LUN)
978 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
979 ControllerType::Nvme,
980 PETRI_NVME_BOOT_VTL2_CONTROLLER,
981 PETRI_NVME_BOOT_NSID,
982 )),
983 )
984 .build(),
985 ),
986 BootDeviceType::Nvme => self
987 .add_vmbus_storage_controller(
988 &PETRI_NVME_BOOT_VTL0_CONTROLLER,
989 Vtl::Vtl0,
990 VmbusStorageType::Nvme,
991 )
992 .add_vmbus_drive(
993 boot_drive,
994 &PETRI_NVME_BOOT_VTL0_CONTROLLER,
995 Some(PETRI_NVME_BOOT_NSID),
996 ),
997 BootDeviceType::NvmeViaScsi => todo!(),
998 BootDeviceType::NvmeViaNvme => todo!(),
999 BootDeviceType::PcieNvme => {
1000 let port_name = self
1001 .pcie_boot_port
1002 .clone()
1003 .unwrap_or_else(|| "s0rc0rp0".into());
1004 self.config.pcie_nvme_drives.push(PcieNvmeDrive {
1005 port_name,
1006 nsid: 1,
1007 drive: boot_drive,
1008 });
1009 self
1010 }
1011 BootDeviceType::PcieVirtioBlk => {
1012 self.config.pcie_virtio_blk_drives.push(PcieVirtioBlkDrive {
1013 port_name: "s0rc0rp0".into(),
1014 drive: boot_drive,
1015 });
1016 self
1017 }
1018 }
1019 } else {
1020 self
1021 }
1022 }
1023
1024 fn has_agent_disk(&self) -> bool {
1029 if self.uses_pipette_as_init() {
1030 self.agent_image.as_ref().is_some_and(|i| i.has_extras())
1031 } else {
1032 self.agent_image.is_some()
1033 }
1034 }
1035
1036 pub fn properties(&self) -> PetriVmProperties {
1038 PetriVmProperties {
1039 is_openhcl: self.config.firmware.is_openhcl(),
1040 is_isolated: self.config.firmware.isolation().is_some(),
1041 is_pcat: self.config.firmware.is_pcat(),
1042 is_linux_direct: self.config.firmware.is_linux_direct(),
1043 using_vtl0_pipette: self.using_vtl0_pipette(),
1044 using_vpci: self.boot_device_type.requires_vpci_boot(),
1045 os_flavor: self.config.firmware.os_flavor(),
1046 minimal_mode: self.minimal_mode,
1047 uses_pipette_as_init: self.uses_pipette_as_init(),
1048 enable_serial: self.enable_serial,
1049 prebuilt_initrd: self.prebuilt_initrd.clone(),
1050 has_agent_disk: self.has_agent_disk(),
1051 use_virtio_vsock: self.use_virtio_vsock,
1052 #[cfg(target_os = "linux")]
1053 vhost_vsock_guest_cid: self.vhost_vsock_guest_cid,
1054 no_vmbus: self.no_vmbus,
1055 no_hv: self.no_hv,
1056 }
1057 }
1058
1059 fn uses_pipette_as_init(&self) -> bool {
1065 self.config.firmware.is_linux_direct()
1066 && !self.config.firmware.is_openhcl()
1067 && self.pipette_binary.is_some()
1068 }
1069
1070 pub fn using_vtl0_pipette(&self) -> bool {
1072 self.uses_pipette_as_init()
1073 || self
1074 .agent_image
1075 .as_ref()
1076 .is_some_and(|x| x.contains_pipette())
1077 }
1078
1079 pub async fn run_without_agent(self) -> anyhow::Result<PetriVm<T>> {
1083 self.run_core().await
1084 }
1085
1086 pub async fn run(self) -> anyhow::Result<(PetriVm<T>, PipetteClient)> {
1089 assert!(self.using_vtl0_pipette());
1090
1091 let mut vm = self.run_core().await?;
1092 let client = vm.wait_for_agent().await?;
1093 Ok((vm, client))
1094 }
1095
1096 async fn run_core(mut self) -> anyhow::Result<PetriVm<T>> {
1097 self = self.add_boot_disk().add_agent_disks();
1100
1101 let _prepared_initrd_guard =
1105 if self.uses_pipette_as_init() && self.prebuilt_initrd.is_none() {
1106 let tmp = self.prepare_initrd()?;
1107 self.prebuilt_initrd = Some(tmp.to_path_buf());
1108 Some(tmp)
1109 } else {
1110 None
1111 };
1112
1113 tracing::debug!(builder = ?self);
1114
1115 let arch = self.config.arch;
1116 let expect_reset = self.expect_reset();
1117 let properties = self.properties();
1118
1119 let (mut runtime, config) = self
1120 .backend
1121 .run(
1122 self.config,
1123 self.modify_vmm_config,
1124 &self.resources,
1125 properties,
1126 )
1127 .await?;
1128 let openhcl_diag_handler = runtime.openhcl_diag();
1129 let watchdog_tasks =
1130 Self::start_watchdog_tasks(&self.resources, &mut runtime, self.enable_screenshots)?;
1131
1132 let mut vm = PetriVm {
1133 resources: self.resources,
1134 runtime,
1135 watchdog_tasks,
1136 openhcl_diag_handler,
1137
1138 arch,
1139 guest_quirks: self.guest_quirks,
1140 vmm_quirks: self.vmm_quirks,
1141 expected_boot_event: self.expected_boot_event,
1142
1143 config,
1144 };
1145
1146 if expect_reset {
1147 vm.wait_for_reset_core().await?;
1148 }
1149
1150 vm.wait_for_expected_boot_event().await?;
1151
1152 Ok(vm)
1153 }
1154
1155 fn expect_reset(&self) -> bool {
1156 self.override_expect_reset
1157 || matches!(
1158 (
1159 self.guest_quirks.initial_reboot,
1160 self.expected_boot_event,
1161 &self.config.firmware,
1162 &self.config.tpm,
1163 ),
1164 (
1165 Some(InitialRebootCondition::Always),
1166 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
1167 _,
1168 _,
1169 ) | (
1170 Some(InitialRebootCondition::WithTpm),
1171 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
1172 _,
1173 Some(_),
1174 )
1175 )
1176 }
1177
1178 fn start_watchdog_tasks(
1179 resources: &PetriVmResources,
1180 runtime: &mut T::VmRuntime,
1181 enable_screenshots: bool,
1182 ) -> anyhow::Result<Vec<Task<()>>> {
1183 let mut tasks = Vec::new();
1184
1185 {
1186 const TIMEOUT_DURATION_MINUTES: u64 = 10;
1187 const TIMER_DURATION: Duration = Duration::from_secs(TIMEOUT_DURATION_MINUTES * 60);
1188 let log_source = resources.log_source.clone();
1189 let inspect_task =
1190 |name,
1191 driver: &DefaultDriver,
1192 inspect: std::pin::Pin<Box<dyn Future<Output = _> + Send>>| {
1193 driver.spawn(format!("petri-watchdog-inspect-{name}"), async move {
1194 if CancelContext::new()
1195 .with_timeout(Duration::from_secs(10))
1196 .until_cancelled(save_inspect(name, inspect, &log_source))
1197 .await
1198 .is_err()
1199 {
1200 tracing::warn!(name, "Failed to collect inspect data within timeout");
1201 }
1202 })
1203 };
1204
1205 let driver = resources.driver.clone();
1206 let vmm_inspector = runtime.inspector();
1207 let openhcl_diag_handler = runtime.openhcl_diag();
1208 tasks.push(resources.driver.spawn("timer-watchdog", async move {
1209 PolledTimer::new(&driver).sleep(TIMER_DURATION).await;
1210 tracing::warn!("Test timeout reached after {TIMEOUT_DURATION_MINUTES} minutes, collecting diagnostics.");
1211 let mut timeout_tasks = Vec::new();
1212 if let Some(inspector) = vmm_inspector {
1213 timeout_tasks.push(inspect_task.clone()("vmm", &driver, Box::pin(async move { inspector.inspect("").await })) );
1214 }
1215 if let Some(openhcl_diag_handler) = openhcl_diag_handler {
1216 timeout_tasks.push(inspect_task("openhcl", &driver, Box::pin(async move { openhcl_diag_handler.inspect("", None, None).await })));
1217 }
1218 futures::future::join_all(timeout_tasks).await;
1219 tracing::error!("Test time out diagnostics collection complete, aborting.");
1220 panic!("Test timed out");
1221 }));
1222 }
1223
1224 if enable_screenshots {
1225 if let Some(mut framebuffer_access) = runtime.take_framebuffer_access() {
1226 let mut timer = PolledTimer::new(&resources.driver);
1227 let log_source = resources.log_source.clone();
1228
1229 tasks.push(
1230 resources
1231 .driver
1232 .spawn("petri-watchdog-screenshot", async move {
1233 let mut image = Vec::new();
1234 let mut last_image = Vec::new();
1235 loop {
1236 timer.sleep(Duration::from_secs(2)).await;
1237 tracing::trace!("Taking screenshot.");
1238
1239 let VmScreenshotMeta {
1240 color,
1241 width,
1242 height,
1243 } = match framebuffer_access.screenshot(&mut image).await {
1244 Ok(Some(meta)) => meta,
1245 Ok(None) => {
1246 tracing::debug!("VM off, skipping screenshot.");
1247 continue;
1248 }
1249 Err(e) => {
1250 tracing::error!(?e, "Failed to take screenshot");
1251 continue;
1252 }
1253 };
1254
1255 if image == last_image {
1256 tracing::debug!(
1257 "No change in framebuffer, skipping screenshot."
1258 );
1259 continue;
1260 }
1261
1262 let r = log_source.create_attachment("screenshot.png").and_then(
1263 |mut f| {
1264 image::write_buffer_with_format(
1265 &mut f,
1266 &image,
1267 width.into(),
1268 height.into(),
1269 color,
1270 image::ImageFormat::Png,
1271 )
1272 .map_err(Into::into)
1273 },
1274 );
1275
1276 if let Err(e) = r {
1277 tracing::error!(?e, "Failed to save screenshot");
1278 } else {
1279 tracing::info!("Screenshot saved.");
1280 }
1281
1282 std::mem::swap(&mut image, &mut last_image);
1283 }
1284 }),
1285 );
1286 }
1287 }
1288
1289 Ok(tasks)
1290 }
1291
1292 pub fn with_expect_boot_failure(mut self) -> Self {
1295 self.expected_boot_event = Some(FirmwareEvent::BootFailed);
1296 self
1297 }
1298
1299 pub fn with_expect_no_boot_event(mut self) -> Self {
1302 self.expected_boot_event = None;
1303 self
1304 }
1305
1306 pub fn with_expect_reset(mut self) -> Self {
1310 self.override_expect_reset = true;
1311 self
1312 }
1313
1314 pub fn with_secure_boot(mut self) -> Self {
1316 self.config
1317 .firmware
1318 .uefi_config_mut()
1319 .expect("Secure boot is only supported for UEFI firmware.")
1320 .secure_boot_enabled = true;
1321
1322 match self.os_flavor() {
1323 OsFlavor::Windows => self.with_windows_secure_boot_template(),
1324 OsFlavor::Linux => self.with_uefi_ca_secure_boot_template(),
1325 _ => panic!(
1326 "Secure boot unsupported for OS flavor {:?}",
1327 self.os_flavor()
1328 ),
1329 }
1330 }
1331
1332 pub fn with_windows_secure_boot_template(mut self) -> Self {
1334 self.config
1335 .firmware
1336 .uefi_config_mut()
1337 .expect("Secure boot is only supported for UEFI firmware.")
1338 .secure_boot_template = Some(SecureBootTemplate::MicrosoftWindows);
1339 self
1340 }
1341
1342 pub fn with_uefi_ca_secure_boot_template(mut self) -> Self {
1344 self.config
1345 .firmware
1346 .uefi_config_mut()
1347 .expect("Secure boot is only supported for UEFI firmware.")
1348 .secure_boot_template = Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority);
1349 self
1350 }
1351
1352 pub fn with_custom_uefi_json(mut self, json: impl Into<Vec<u8>>) -> Self {
1354 self.config
1355 .firmware
1356 .uefi_config_mut()
1357 .expect("Custom UEFI variables are only supported for UEFI firmware.")
1358 .custom_uefi_json = Some(json.into());
1359 self
1360 }
1361
1362 pub fn with_processor_topology(mut self, topology: ProcessorTopology) -> Self {
1364 self.config.proc_topology = topology;
1365 self
1366 }
1367
1368 pub fn with_memory(mut self, memory: MemoryConfig) -> Self {
1370 self.config.memory = memory;
1371 self
1372 }
1373
1374 pub fn with_vtl2_base_address_type(mut self, address_type: Vtl2BaseAddressType) -> Self {
1379 self.config
1380 .firmware
1381 .openhcl_config_mut()
1382 .expect("OpenHCL firmware is required to set custom VTL2 address type.")
1383 .vtl2_base_address_type = Some(address_type);
1384 self
1385 }
1386
1387 pub fn with_custom_openhcl(mut self, artifact: ResolvedArtifact<impl IsOpenhclIgvm>) -> Self {
1389 match &mut self.config.firmware {
1390 Firmware::OpenhclLinuxDirect { igvm_path, .. }
1391 | Firmware::OpenhclPcat { igvm_path, .. }
1392 | Firmware::OpenhclUefi { igvm_path, .. } => {
1393 *igvm_path = artifact.erase();
1394 }
1395 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } | Firmware::Pcat { .. } => {
1396 panic!("Custom OpenHCL is only supported for OpenHCL firmware.")
1397 }
1398 }
1399 self
1400 }
1401
1402 pub fn with_openhcl_command_line(mut self, additional_command_line: &str) -> Self {
1404 append_cmdline(
1405 &mut self
1406 .config
1407 .firmware
1408 .openhcl_config_mut()
1409 .expect("OpenHCL command line is only supported for OpenHCL firmware.")
1410 .custom_command_line,
1411 additional_command_line,
1412 );
1413 self
1414 }
1415
1416 pub fn with_mana_keepalive(mut self, enable: bool) -> Self {
1418 self.config
1419 .firmware
1420 .openhcl_config_mut()
1421 .expect("MANA keepalive is only supported for OpenHCL firmware.")
1422 .enable_mana_keepalive = enable;
1423 self
1424 }
1425
1426 pub fn with_confidential_filtering(self) -> Self {
1428 if !self.config.firmware.is_openhcl() {
1429 panic!("Confidential filtering is only supported for OpenHCL");
1430 }
1431 self.with_openhcl_command_line(&format!(
1432 "{}=1 {}=0",
1433 underhill_confidentiality::OPENHCL_CONFIDENTIAL_ENV_VAR_NAME,
1434 underhill_confidentiality::OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME
1435 ))
1436 }
1437
1438 pub fn with_openhcl_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1440 self.config
1441 .firmware
1442 .openhcl_config_mut()
1443 .expect("OpenHCL firmware is required to set custom OpenHCL log levels.")
1444 .log_levels = levels;
1445 self
1446 }
1447
1448 pub fn with_host_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1452 if let OpenvmmLogConfig::Custom(ref custom_levels) = levels {
1453 for key in custom_levels.keys() {
1454 if !["OPENVMM_LOG", "OPENVMM_SHOW_SPANS"].contains(&key.as_str()) {
1455 panic!("Unsupported OpenVMM log level key: {}", key);
1456 }
1457 }
1458 }
1459
1460 self.config.host_log_levels = Some(levels.clone());
1461 self
1462 }
1463
1464 pub fn with_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
1466 self.agent_image
1467 .as_mut()
1468 .expect("no guest pipette")
1469 .add_file(name, artifact);
1470 self
1471 }
1472
1473 pub fn with_openhcl_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
1475 self.openhcl_agent_image
1476 .as_mut()
1477 .expect("no openhcl pipette")
1478 .add_file(name, artifact);
1479 self
1480 }
1481
1482 pub fn with_uefi_frontpage(mut self, enable: bool) -> Self {
1484 self.config
1485 .firmware
1486 .uefi_config_mut()
1487 .expect("UEFI frontpage is only supported for UEFI firmware.")
1488 .disable_frontpage = !enable;
1489 self
1490 }
1491
1492 pub fn with_efi_diagnostics_log_level(mut self, level: EfiDiagnosticsLogLevel) -> Self {
1498 self.config
1499 .firmware
1500 .uefi_config_mut()
1501 .expect("EFI diagnostics log level is only supported for UEFI firmware.")
1502 .efi_diagnostics_log_level = level;
1503 self
1504 }
1505
1506 pub fn with_efi_diagnostics_rate_limit(mut self, limit: u32) -> Self {
1512 self.config
1513 .firmware
1514 .uefi_config_mut()
1515 .expect("EFI diagnostics rate limit is only supported for UEFI firmware.")
1516 .efi_diagnostics_rate_limit = Some(limit);
1517 self
1518 }
1519
1520 pub fn with_default_boot_always_attempt(mut self, enable: bool) -> Self {
1522 self.config
1523 .firmware
1524 .uefi_config_mut()
1525 .expect("Default boot always attempt is only supported for UEFI firmware.")
1526 .default_boot_always_attempt = enable;
1527 self
1528 }
1529
1530 pub fn with_uefi_force_dma_bounce(mut self, enable: bool) -> Self {
1532 self.config
1533 .firmware
1534 .uefi_config_mut()
1535 .expect("force DMA bounce is only supported for UEFI firmware.")
1536 .force_dma_bounce = enable;
1537 self
1538 }
1539
1540 pub fn with_vmbus_redirect(mut self, enable: bool) -> Self {
1542 self.config
1543 .firmware
1544 .openhcl_config_mut()
1545 .expect("VMBus redirection is only supported for OpenHCL firmware.")
1546 .vmbus_redirect = enable;
1547 self
1548 }
1549
1550 pub fn with_hibernation_enabled(mut self, enable: bool) -> Self {
1556 self.config.hibernation_enabled = enable;
1557 self
1558 }
1559
1560 pub fn with_ipmi(mut self, enable: bool) -> Self {
1562 self.config.ipmi_enabled = enable;
1563 self
1564 }
1565
1566 pub fn with_guest_state_lifetime(
1568 mut self,
1569 guest_state_lifetime: PetriGuestStateLifetime,
1570 ) -> Self {
1571 let disk = match self.config.vmgs {
1572 PetriVmgsResource::Disk(disk)
1573 | PetriVmgsResource::ReprovisionOnFailure(disk)
1574 | PetriVmgsResource::Reprovision(disk) => disk,
1575 PetriVmgsResource::Ephemeral => PetriVmgsDisk::default(),
1576 };
1577 self.config.vmgs = match guest_state_lifetime {
1578 PetriGuestStateLifetime::Disk => PetriVmgsResource::Disk(disk),
1579 PetriGuestStateLifetime::ReprovisionOnFailure => {
1580 PetriVmgsResource::ReprovisionOnFailure(disk)
1581 }
1582 PetriGuestStateLifetime::Reprovision => PetriVmgsResource::Reprovision(disk),
1583 PetriGuestStateLifetime::Ephemeral => {
1584 if !matches!(disk.disk, Disk::Memory(_)) {
1585 panic!("attempted to use ephemeral guest state after specifying backing vmgs")
1586 }
1587 PetriVmgsResource::Ephemeral
1588 }
1589 };
1590 self
1591 }
1592
1593 pub fn with_guest_state_encryption(mut self, policy: GuestStateEncryptionPolicy) -> Self {
1595 match &mut self.config.vmgs {
1596 PetriVmgsResource::Disk(vmgs)
1597 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1598 | PetriVmgsResource::Reprovision(vmgs) => {
1599 vmgs.encryption_policy = policy;
1600 }
1601 PetriVmgsResource::Ephemeral => {
1602 panic!("attempted to encrypt ephemeral guest state")
1603 }
1604 }
1605 self
1606 }
1607
1608 pub fn with_initial_vmgs(self, disk: ResolvedArtifact<impl IsTestVmgs>) -> Self {
1610 self.with_backing_vmgs(Disk::Differencing(DiskPath::Local(disk.into())))
1611 }
1612
1613 pub fn with_persistent_vmgs(self, disk: impl AsRef<Path>) -> Self {
1615 self.with_backing_vmgs(Disk::Persistent(disk.as_ref().to_path_buf()))
1616 }
1617
1618 fn with_backing_vmgs(mut self, disk: Disk) -> Self {
1619 match &mut self.config.vmgs {
1620 PetriVmgsResource::Disk(vmgs)
1621 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1622 | PetriVmgsResource::Reprovision(vmgs) => {
1623 if !matches!(vmgs.disk, Disk::Memory(_)) {
1624 panic!("already specified a backing vmgs file");
1625 }
1626 vmgs.disk = disk;
1627 }
1628 PetriVmgsResource::Ephemeral => {
1629 panic!("attempted to specify a backing vmgs with ephemeral guest state")
1630 }
1631 }
1632 self
1633 }
1634
1635 pub fn with_boot_device_type(mut self, boot: BootDeviceType) -> Self {
1639 self.boot_device_type = boot;
1640 self
1641 }
1642
1643 pub fn with_pcie_boot_port(mut self, port_name: &str) -> Self {
1649 self.pcie_boot_port = Some(port_name.to_string());
1650 self
1651 }
1652
1653 pub fn with_tpm(mut self, enable: bool) -> Self {
1655 if enable {
1656 self.config.tpm.get_or_insert_default();
1657 } else {
1658 self.config.tpm = None;
1659 }
1660 self
1661 }
1662
1663 pub fn with_tpm_state_persistence(mut self, tpm_state_persistence: bool) -> Self {
1665 self.config
1666 .tpm
1667 .as_mut()
1668 .expect("TPM persistence requires a TPM")
1669 .no_persistent_secrets = !tpm_state_persistence;
1670 self
1671 }
1672
1673 pub fn with_hardware_sealing_policy(mut self, policy: PetriHardwareSealingPolicy) -> Self {
1675 self.config
1676 .tpm
1677 .as_mut()
1678 .expect("hardware sealing policy requires a TPM")
1679 .hardware_sealing_policy = policy;
1680 self
1681 }
1682
1683 pub fn with_tpm_version(mut self, version: PetriTpmVersion) -> Self {
1685 self.config
1686 .tpm
1687 .as_mut()
1688 .expect("TPM version requires a TPM")
1689 .version = version;
1690 self
1691 }
1692
1693 pub fn with_custom_vtl2_settings(
1697 mut self,
1698 f: impl FnOnce(&mut Vtl2Settings) + 'static + Send + Sync,
1699 ) -> Self {
1700 f(self
1701 .config
1702 .firmware
1703 .vtl2_settings()
1704 .expect("Custom VTL 2 settings are only supported with OpenHCL"));
1705 self
1706 }
1707
1708 pub fn add_vtl2_storage_controller(self, controller: StorageController) -> Self {
1710 self.with_custom_vtl2_settings(move |v| {
1711 v.dynamic
1712 .as_mut()
1713 .unwrap()
1714 .storage_controllers
1715 .push(controller)
1716 })
1717 }
1718
1719 pub fn add_vmbus_storage_controller(
1721 mut self,
1722 id: &Guid,
1723 target_vtl: Vtl,
1724 controller_type: VmbusStorageType,
1725 ) -> Self {
1726 if self
1727 .config
1728 .vmbus_storage_controllers
1729 .insert(
1730 *id,
1731 VmbusStorageController::new(target_vtl, controller_type),
1732 )
1733 .is_some()
1734 {
1735 panic!("storage controller {id} already existed");
1736 }
1737 self
1738 }
1739
1740 pub fn add_vmbus_drive(
1742 mut self,
1743 drive: Drive,
1744 controller_id: &Guid,
1745 controller_location: Option<u32>,
1746 ) -> Self {
1747 let controller = self
1748 .config
1749 .vmbus_storage_controllers
1750 .get_mut(controller_id)
1751 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
1752
1753 _ = controller.set_drive(controller_location, drive, false);
1754
1755 self
1756 }
1757
1758 pub fn add_ide_drive(
1760 mut self,
1761 drive: Drive,
1762 controller_number: u32,
1763 controller_location: u8,
1764 ) -> Self {
1765 self.config
1766 .firmware
1767 .ide_controllers_mut()
1768 .expect("Host IDE requires PCAT with no HCL")[controller_number as usize]
1769 [controller_location as usize] = Some(drive);
1770
1771 self
1772 }
1773
1774 pub fn add_physical_nvme_device(mut self, vsid: Guid, device: PhysicalNvmeDevice) -> Self {
1776 if self
1777 .config
1778 .physical_nvme_devices
1779 .insert(vsid, device)
1780 .is_some()
1781 {
1782 panic!("physical NVMe device {vsid} already existed");
1783 }
1784 self
1785 }
1786
1787 pub fn os_flavor(&self) -> OsFlavor {
1789 self.config.firmware.os_flavor()
1790 }
1791
1792 pub fn is_openhcl(&self) -> bool {
1794 self.config.firmware.is_openhcl()
1795 }
1796
1797 pub fn isolation(&self) -> Option<IsolationType> {
1799 self.config.firmware.isolation()
1800 }
1801
1802 pub fn arch(&self) -> MachineArch {
1804 self.config.arch
1805 }
1806
1807 pub fn log_source(&self) -> &PetriLogSource {
1809 &self.resources.log_source
1810 }
1811
1812 pub fn default_servicing_flags(&self) -> OpenHclServicingFlags {
1814 T::default_servicing_flags()
1815 }
1816
1817 pub fn modify_backend(
1819 mut self,
1820 f: impl FnOnce(T::VmmConfig) -> T::VmmConfig + 'static + Send,
1821 ) -> Self {
1822 if self.modify_vmm_config.is_some() {
1823 panic!("only one modify_backend allowed");
1824 }
1825 self.modify_vmm_config = Some(ModifyFn(Box::new(f)));
1826 self
1827 }
1828}
1829
1830impl<T: PetriVmmBackend> PetriVm<T> {
1831 pub async fn teardown(self) -> anyhow::Result<()> {
1833 tracing::info!("Tearing down VM...");
1834 self.runtime.teardown().await
1835 }
1836
1837 pub async fn wait_for_halt(&mut self) -> anyhow::Result<PetriHaltReasonDetail> {
1839 tracing::info!("Waiting for VM to halt...");
1840 let halt_reason = self.runtime.wait_for_halt(false).await?;
1841 tracing::info!("VM halted: {halt_reason:?}. Cancelling watchdogs...");
1842 futures::future::join_all(self.watchdog_tasks.drain(..).map(|t| t.cancel())).await;
1843 Ok(halt_reason)
1844 }
1845
1846 pub async fn wait_for_clean_shutdown(&mut self) -> anyhow::Result<()> {
1848 let halt_reason = self.wait_for_halt().await?;
1849 if halt_reason.reason != PetriHaltReason::PowerOff {
1850 anyhow::bail!("Expected PowerOff, got {halt_reason:?}");
1851 }
1852 tracing::info!("VM was cleanly powered off and torn down.");
1853 Ok(())
1854 }
1855
1856 pub async fn wait_for_teardown(mut self) -> anyhow::Result<PetriHaltReasonDetail> {
1859 let halt_reason = self.wait_for_halt().await?;
1860 self.teardown().await?;
1861 Ok(halt_reason)
1862 }
1863
1864 pub async fn wait_for_clean_teardown(mut self) -> anyhow::Result<()> {
1866 self.wait_for_clean_shutdown().await?;
1867 self.teardown().await
1868 }
1869
1870 pub async fn wait_for_reset_no_agent(&mut self) -> anyhow::Result<()> {
1872 self.wait_for_reset_core().await?;
1873 self.wait_for_expected_boot_event().await?;
1874 Ok(())
1875 }
1876
1877 pub async fn wait_for_reset(&mut self) -> anyhow::Result<PipetteClient> {
1879 self.wait_for_reset_no_agent().await?;
1880 self.wait_for_agent().await
1881 }
1882
1883 async fn wait_for_reset_core(&mut self) -> anyhow::Result<()> {
1884 tracing::info!("Waiting for VM to reset...");
1885 let halt_reason = self.runtime.wait_for_halt(true).await?;
1886 if halt_reason.reason != PetriHaltReason::Reset {
1887 anyhow::bail!("Expected reset, got {halt_reason:?}");
1888 }
1889 tracing::info!("VM reset.");
1890 Ok(())
1891 }
1892
1893 pub async fn inspect_openhcl(
1904 &self,
1905 path: impl Into<String>,
1906 depth: Option<usize>,
1907 timeout: Option<Duration>,
1908 ) -> anyhow::Result<inspect::Node> {
1909 self.openhcl_diag()?
1910 .inspect(path.into().as_str(), depth, timeout)
1911 .await
1912 }
1913
1914 pub async fn inspect_update_openhcl(
1924 &self,
1925 path: impl Into<String>,
1926 value: impl Into<String>,
1927 ) -> anyhow::Result<inspect::Value> {
1928 self.openhcl_diag()?
1929 .inspect_update(path.into(), value.into())
1930 .await
1931 }
1932
1933 pub async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()> {
1935 self.inspect_openhcl("", None, None).await.map(|_| ())
1936 }
1937
1938 pub async fn inspect_vmm(&self, path: &str) -> anyhow::Result<inspect::Node> {
1949 use anyhow::Context;
1950
1951 let inspector = self
1952 .runtime
1953 .inspector()
1954 .context("this VMM backend does not support inspect")?;
1955 inspector.inspect(path).await
1956 }
1957
1958 pub async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()> {
1964 self.openhcl_diag()?.wait_for_vtl2().await
1965 }
1966
1967 pub async fn kmsg(&self) -> anyhow::Result<diag_client::kmsg_stream::KmsgStream> {
1969 self.openhcl_diag()?.kmsg().await
1970 }
1971
1972 pub async fn openhcl_core_dump(&self, name: &str, path: &Path) -> anyhow::Result<()> {
1975 self.openhcl_diag()?.core_dump(name, path).await
1976 }
1977
1978 pub async fn openhcl_crash(&self, name: &str) -> anyhow::Result<()> {
1980 self.openhcl_diag()?.crash(name).await
1981 }
1982
1983 async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient> {
1986 self.runtime.wait_for_enlightened_shutdown_ready().await?;
1996 self.runtime.wait_for_agent(false).await
1997 }
1998
1999 pub async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient> {
2003 self.launch_vtl2_pipette().await?;
2005 self.runtime.wait_for_agent(true).await
2006 }
2007
2008 async fn wait_for_expected_boot_event(&mut self) -> anyhow::Result<()> {
2015 if let Some(expected_event) = self.expected_boot_event {
2016 let event = self.wait_for_boot_event().await?;
2017
2018 anyhow::ensure!(
2019 event == expected_event,
2020 "Did not receive expected boot event"
2021 );
2022 } else {
2023 tracing::warn!("Boot event not emitted for configured firmware or manually ignored.");
2024 }
2025
2026 Ok(())
2027 }
2028
2029 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
2032 tracing::info!("Waiting for boot event...");
2033 let boot_event = loop {
2034 if let Some(event) = self
2035 .runtime
2036 .wait_for_boot_event(self.vmm_quirks.flaky_boot)
2037 .await?
2038 {
2039 break event;
2040 }
2041
2042 tracing::error!("Did not get boot event in required time, resetting...");
2043 if let Some(inspector) = self.runtime.inspector() {
2044 save_inspect(
2045 "vmm",
2046 Box::pin(async move { inspector.inspect("").await }),
2047 &self.resources.log_source,
2048 )
2049 .await;
2050 }
2051
2052 self.runtime.reset().await?;
2053 };
2054 tracing::info!("Got boot event: {boot_event:?}");
2055 Ok(boot_event)
2056 }
2057
2058 pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
2061 tracing::info!("Waiting for enlightened shutdown to be ready");
2062 self.runtime.wait_for_enlightened_shutdown_ready().await?;
2063
2064 let mut wait_time = Duration::from_secs(10);
2070
2071 if let Some(duration) = self.guest_quirks.hyperv_shutdown_ic_sleep {
2073 wait_time += duration;
2074 }
2075
2076 tracing::info!(
2077 "Shutdown IC reported ready, waiting for an extra {}s",
2078 wait_time.as_secs()
2079 );
2080 PolledTimer::new(&self.resources.driver)
2081 .sleep(wait_time)
2082 .await;
2083
2084 tracing::info!("Sending enlightened shutdown command");
2085 self.runtime.send_enlightened_shutdown(kind).await
2086 }
2087
2088 pub async fn restart_openhcl(
2091 &mut self,
2092 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
2093 flags: OpenHclServicingFlags,
2094 ) -> anyhow::Result<()> {
2095 self.runtime
2096 .restart_openhcl(&new_openhcl.erase(), flags)
2097 .await
2098 }
2099
2100 pub async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()> {
2103 self.runtime.update_command_line(command_line).await
2104 }
2105
2106 pub async fn add_pcie_device(
2108 &mut self,
2109 port_name: String,
2110 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
2111 ) -> anyhow::Result<()> {
2112 self.runtime.add_pcie_device(port_name, resource).await
2113 }
2114
2115 pub async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
2117 self.runtime.remove_pcie_device(port_name).await
2118 }
2119
2120 pub async fn save_openhcl(
2123 &mut self,
2124 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
2125 flags: OpenHclServicingFlags,
2126 ) -> anyhow::Result<()> {
2127 self.runtime.save_openhcl(&new_openhcl.erase(), flags).await
2128 }
2129
2130 pub async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
2133 self.runtime.restore_openhcl().await
2134 }
2135
2136 pub fn arch(&self) -> MachineArch {
2138 self.arch
2139 }
2140
2141 pub fn backend(&mut self) -> &mut T::VmRuntime {
2143 &mut self.runtime
2144 }
2145
2146 async fn launch_vtl2_pipette(&self) -> anyhow::Result<()> {
2147 tracing::debug!("Launching VTL 2 pipette...");
2148
2149 let res = self
2151 .openhcl_diag()?
2152 .run_vtl2_command("sh", &["-c", "mkdir /cidata && mount LABEL=cidata /cidata"])
2153 .await?;
2154
2155 if !res.exit_status.success() {
2156 anyhow::bail!("Failed to mount VTL 2 pipette drive: {:?}", res);
2157 }
2158
2159 let res = self
2160 .openhcl_diag()?
2161 .run_detached_vtl2_command("sh", &["-c", "/cidata/pipette 2>&1 | logger &"])
2162 .await?;
2163
2164 if !res.success() {
2165 anyhow::bail!("Failed to spawn VTL 2 pipette: {:?}", res);
2166 }
2167
2168 Ok(())
2169 }
2170
2171 fn openhcl_diag(&self) -> anyhow::Result<&OpenHclDiagHandler> {
2172 if let Some(ohd) = self.openhcl_diag_handler.as_ref() {
2173 Ok(ohd)
2174 } else {
2175 anyhow::bail!("VM is not configured with OpenHCL")
2176 }
2177 }
2178
2179 pub async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2181 self.runtime.get_guest_state_file().await
2182 }
2183
2184 pub async fn modify_vtl2_settings(
2186 &mut self,
2187 f: impl FnOnce(&mut Vtl2Settings),
2188 ) -> anyhow::Result<()> {
2189 if self.openhcl_diag_handler.is_none() {
2190 panic!("Custom VTL 2 settings are only supported with OpenHCL");
2191 }
2192 f(self
2193 .config
2194 .vtl2_settings
2195 .get_or_insert_with(default_vtl2_settings));
2196 self.runtime
2197 .set_vtl2_settings(self.config.vtl2_settings.as_ref().unwrap())
2198 .await
2199 }
2200
2201 pub fn get_vmbus_storage_controllers(&self) -> &HashMap<Guid, VmbusStorageController> {
2203 &self.config.vmbus_storage_controllers
2204 }
2205
2206 pub async fn set_vmbus_drive(
2208 &mut self,
2209 drive: Drive,
2210 controller_id: &Guid,
2211 controller_location: Option<u32>,
2212 ) -> anyhow::Result<()> {
2213 let controller = self
2214 .config
2215 .vmbus_storage_controllers
2216 .get_mut(controller_id)
2217 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
2218
2219 let controller_location = controller.set_drive(controller_location, drive, true);
2220 let disk = controller.drives.get(&controller_location).unwrap();
2221
2222 self.runtime
2223 .set_vmbus_drive(disk, controller_id, controller_location)
2224 .await?;
2225
2226 Ok(())
2227 }
2228}
2229
2230#[async_trait]
2232pub trait PetriVmRuntime: Send + Sync + 'static {
2233 type VmInspector: PetriVmInspector;
2235 type VmFramebufferAccess: PetriVmFramebufferAccess;
2237
2238 async fn teardown(self) -> anyhow::Result<()>;
2240 async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReasonDetail>;
2243 async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>;
2245 fn openhcl_diag(&self) -> Option<OpenHclDiagHandler>;
2247 async fn wait_for_boot_event(
2250 &mut self,
2251 timeout: Option<Duration>,
2252 ) -> anyhow::Result<Option<FirmwareEvent>>;
2253 async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()>;
2256 async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
2258 async fn restart_openhcl(
2261 &mut self,
2262 new_openhcl: &ResolvedArtifact,
2263 flags: OpenHclServicingFlags,
2264 ) -> anyhow::Result<()>;
2265 async fn save_openhcl(
2269 &mut self,
2270 new_openhcl: &ResolvedArtifact,
2271 flags: OpenHclServicingFlags,
2272 ) -> anyhow::Result<()>;
2273 async fn restore_openhcl(&mut self) -> anyhow::Result<()>;
2276 async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()>;
2279 fn inspector(&self) -> Option<Self::VmInspector> {
2281 None
2282 }
2283 fn take_framebuffer_access(&mut self) -> Option<Self::VmFramebufferAccess> {
2286 None
2287 }
2288 async fn reset(&mut self) -> anyhow::Result<()>;
2290 async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2292 Ok(None)
2293 }
2294 async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()>;
2296 async fn set_vmbus_drive(
2298 &mut self,
2299 disk: &Drive,
2300 controller_id: &Guid,
2301 controller_location: u32,
2302 ) -> anyhow::Result<()>;
2303 async fn add_pcie_device(
2305 &mut self,
2306 port_name: String,
2307 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
2308 ) -> anyhow::Result<()> {
2309 let _ = (port_name, resource);
2310 anyhow::bail!("PCIe hotplug not supported by this backend")
2311 }
2312 async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
2314 let _ = port_name;
2315 anyhow::bail!("PCIe hotplug not supported by this backend")
2316 }
2317}
2318
2319#[async_trait]
2321pub trait PetriVmInspector: Send + Sync + 'static {
2322 async fn inspect(&self, path: &str) -> anyhow::Result<inspect::Node>;
2325}
2326
2327pub struct NoPetriVmInspector;
2329#[async_trait]
2330impl PetriVmInspector for NoPetriVmInspector {
2331 async fn inspect(&self, _path: &str) -> anyhow::Result<inspect::Node> {
2332 unreachable!()
2333 }
2334}
2335
2336pub struct VmScreenshotMeta {
2338 pub color: image::ExtendedColorType,
2340 pub width: u16,
2342 pub height: u16,
2344}
2345
2346#[async_trait]
2348pub trait PetriVmFramebufferAccess: Send + 'static {
2349 async fn screenshot(&mut self, image: &mut Vec<u8>)
2352 -> anyhow::Result<Option<VmScreenshotMeta>>;
2353}
2354
2355#[derive(Debug)]
2357pub struct ProcessorTopology {
2358 pub vp_count: u32,
2360 pub enable_smt: Option<bool>,
2362 pub vps_per_socket: Option<u32>,
2364 pub apic_mode: Option<ApicMode>,
2366}
2367
2368impl Default for ProcessorTopology {
2369 fn default() -> Self {
2370 Self {
2371 vp_count: 2,
2372 enable_smt: None,
2373 vps_per_socket: None,
2374 apic_mode: None,
2375 }
2376 }
2377}
2378
2379impl ProcessorTopology {
2380 pub fn heavy() -> Self {
2382 Self {
2383 vp_count: 16,
2384 vps_per_socket: Some(8),
2385 ..Default::default()
2386 }
2387 }
2388
2389 pub fn very_heavy() -> Self {
2391 Self {
2392 vp_count: 32,
2393 vps_per_socket: Some(16),
2394 ..Default::default()
2395 }
2396 }
2397}
2398
2399#[derive(Debug, Clone, Copy)]
2401pub enum ApicMode {
2402 Xapic,
2404 X2apicSupported,
2406 X2apicEnabled,
2408}
2409
2410#[derive(Debug)]
2412pub struct MemoryConfig {
2413 pub startup_bytes: u64,
2416 pub dynamic_memory_range: Option<(u64, u64)>,
2420 pub numa_mem_sizes: Option<Vec<u64>>,
2423 pub private_memory: Option<bool>,
2442 pub transparent_hugepages: bool,
2454}
2455
2456impl Default for MemoryConfig {
2457 fn default() -> Self {
2458 Self {
2459 startup_bytes: 4 * 1024 * 1024 * 1024, dynamic_memory_range: None,
2461 numa_mem_sizes: None,
2462 private_memory: None,
2463 transparent_hugepages: true,
2464 }
2465 }
2466}
2467
2468#[derive(Debug)]
2470pub struct UefiConfig {
2471 pub secure_boot_enabled: bool,
2473 pub secure_boot_template: Option<SecureBootTemplate>,
2475 pub custom_uefi_json: Option<Vec<u8>>,
2477 pub disable_frontpage: bool,
2479 pub default_boot_always_attempt: bool,
2481 pub enable_vpci_boot: bool,
2483 pub force_dma_bounce: bool,
2485 pub efi_diagnostics_log_level: EfiDiagnosticsLogLevel,
2487 pub efi_diagnostics_rate_limit: Option<u32>,
2490}
2491
2492impl Default for UefiConfig {
2493 fn default() -> Self {
2494 Self {
2495 secure_boot_enabled: false,
2496 secure_boot_template: None,
2497 custom_uefi_json: None,
2498 disable_frontpage: true,
2499 default_boot_always_attempt: false,
2500 enable_vpci_boot: false,
2501 force_dma_bounce: false,
2502 efi_diagnostics_log_level: EfiDiagnosticsLogLevel::Default,
2503 efi_diagnostics_rate_limit: None,
2504 }
2505 }
2506}
2507
2508#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2513pub enum EfiDiagnosticsLogLevel {
2514 #[default]
2516 Default,
2517 Info,
2519 Full,
2521}
2522
2523#[derive(Debug, Clone)]
2525pub enum OpenvmmLogConfig {
2526 TestDefault,
2530 BuiltInDefault,
2533 Custom(BTreeMap<String, String>),
2543}
2544
2545#[derive(Debug)]
2547pub struct OpenHclConfig {
2548 pub vmbus_redirect: bool,
2550 pub enable_mana_keepalive: bool,
2552 pub custom_command_line: Option<String>,
2556 pub log_levels: OpenvmmLogConfig,
2560 pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
2563 pub vtl2_settings: Option<Vtl2Settings>,
2565}
2566
2567impl OpenHclConfig {
2568 pub fn command_line(&self) -> String {
2571 let mut cmdline = self.custom_command_line.clone();
2572
2573 if self.enable_mana_keepalive {
2574 append_cmdline(&mut cmdline, "OPENHCL_MANA_KEEP_ALIVE=host,privatepool");
2575 }
2576
2577 match &self.log_levels {
2578 OpenvmmLogConfig::TestDefault => {
2579 let default_log_levels = {
2580 let openhcl_tracing = if let Ok(x) =
2582 std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG"))
2583 {
2584 format!("OPENVMM_LOG={x}")
2585 } else {
2586 "OPENVMM_LOG=debug".to_owned()
2587 };
2588 let openhcl_show_spans = if let Ok(x) = std::env::var("OPENVMM_SHOW_SPANS") {
2589 format!("OPENVMM_SHOW_SPANS={x}")
2590 } else {
2591 "OPENVMM_SHOW_SPANS=true".to_owned()
2592 };
2593 format!("{openhcl_tracing} {openhcl_show_spans}")
2594 };
2595 append_cmdline(&mut cmdline, &default_log_levels);
2596 }
2597 OpenvmmLogConfig::BuiltInDefault => {
2598 }
2600 OpenvmmLogConfig::Custom(levels) => {
2601 levels.iter().for_each(|(key, value)| {
2602 append_cmdline(&mut cmdline, format!("{key}={value}"));
2603 });
2604 }
2605 }
2606
2607 cmdline.unwrap_or_default()
2608 }
2609}
2610
2611impl Default for OpenHclConfig {
2612 fn default() -> Self {
2613 Self {
2614 vmbus_redirect: false,
2615 enable_mana_keepalive: true,
2616 custom_command_line: None,
2617 log_levels: OpenvmmLogConfig::TestDefault,
2618 vtl2_base_address_type: None,
2619 vtl2_settings: None,
2620 }
2621 }
2622}
2623
2624#[derive(Debug)]
2626pub struct TpmConfig {
2627 pub no_persistent_secrets: bool,
2629 pub hardware_sealing_policy: PetriHardwareSealingPolicy,
2631 pub version: PetriTpmVersion,
2633}
2634
2635impl Default for TpmConfig {
2636 fn default() -> Self {
2637 Self {
2638 no_persistent_secrets: true,
2639 hardware_sealing_policy: PetriHardwareSealingPolicy::Default,
2640 version: PetriTpmVersion::default(),
2641 }
2642 }
2643}
2644
2645#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2647pub enum PetriTpmVersion {
2648 V138,
2650 #[default]
2652 V185,
2653}
2654
2655impl From<PetriTpmVersion> for tpm_resources::TpmVersion {
2656 fn from(version: PetriTpmVersion) -> Self {
2657 match version {
2658 PetriTpmVersion::V138 => tpm_resources::TpmVersion::V138,
2659 PetriTpmVersion::V185 => tpm_resources::TpmVersion::V185,
2660 }
2661 }
2662}
2663
2664impl From<PetriTpmVersion> for get_resources::ged::GedTpmVersion {
2665 fn from(version: PetriTpmVersion) -> Self {
2666 match version {
2667 PetriTpmVersion::V138 => get_resources::ged::GedTpmVersion::V138,
2668 PetriTpmVersion::V185 => get_resources::ged::GedTpmVersion::V185,
2669 }
2670 }
2671}
2672
2673#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2678pub enum PetriHardwareSealingPolicy {
2679 #[default]
2681 Default,
2682 HashPolicy,
2684 SignerPolicy,
2686}
2687
2688#[derive(Debug)]
2692pub enum Firmware {
2693 LinuxDirect {
2695 kernel: ResolvedArtifact,
2697 initrd: ResolvedArtifact,
2699 },
2700 OpenhclLinuxDirect {
2702 igvm_path: ResolvedArtifact,
2704 openhcl_config: OpenHclConfig,
2706 },
2707 Pcat {
2709 guest: PcatGuest,
2711 bios_firmware: ResolvedOptionalArtifact,
2713 svga_firmware: ResolvedOptionalArtifact,
2715 ide_controllers: [[Option<Drive>; 2]; 2],
2717 },
2718 OpenhclPcat {
2720 guest: PcatGuest,
2722 igvm_path: ResolvedArtifact,
2724 bios_firmware: ResolvedOptionalArtifact,
2726 svga_firmware: ResolvedOptionalArtifact,
2728 openhcl_config: OpenHclConfig,
2730 },
2731 Uefi {
2733 guest: UefiGuest,
2735 uefi_firmware: ResolvedArtifact,
2737 uefi_config: UefiConfig,
2739 },
2740 OpenhclUefi {
2742 guest: UefiGuest,
2744 isolation: Option<IsolationType>,
2746 igvm_path: ResolvedArtifact,
2748 uefi_config: UefiConfig,
2750 openhcl_config: OpenHclConfig,
2752 },
2753}
2754
2755#[derive(Debug, Copy, Clone, PartialEq, Eq)]
2757pub enum BootDeviceType {
2758 None,
2760 Ide,
2762 IdeViaScsi,
2764 IdeViaNvme,
2766 Scsi,
2768 ScsiViaScsi,
2770 ScsiViaNvme,
2772 Nvme,
2774 NvmeViaScsi,
2776 NvmeViaNvme,
2778 PcieNvme,
2780 PcieVirtioBlk,
2782}
2783
2784impl BootDeviceType {
2785 fn requires_vtl2(&self) -> bool {
2786 match self {
2787 BootDeviceType::None
2788 | BootDeviceType::Ide
2789 | BootDeviceType::Scsi
2790 | BootDeviceType::Nvme
2791 | BootDeviceType::PcieNvme
2792 | BootDeviceType::PcieVirtioBlk => false,
2793 BootDeviceType::IdeViaScsi
2794 | BootDeviceType::IdeViaNvme
2795 | BootDeviceType::ScsiViaScsi
2796 | BootDeviceType::ScsiViaNvme
2797 | BootDeviceType::NvmeViaScsi
2798 | BootDeviceType::NvmeViaNvme => true,
2799 }
2800 }
2801
2802 fn requires_vpci_boot(&self) -> bool {
2803 matches!(
2804 self,
2805 BootDeviceType::Nvme | BootDeviceType::NvmeViaScsi | BootDeviceType::NvmeViaNvme
2806 )
2807 }
2808
2809 fn requires_vmbus(&self) -> bool {
2810 match self {
2811 BootDeviceType::None
2812 | BootDeviceType::Ide
2813 | BootDeviceType::PcieNvme
2814 | BootDeviceType::PcieVirtioBlk => false,
2815 BootDeviceType::IdeViaScsi
2816 | BootDeviceType::IdeViaNvme
2817 | BootDeviceType::Scsi
2818 | BootDeviceType::ScsiViaScsi
2819 | BootDeviceType::ScsiViaNvme
2820 | BootDeviceType::Nvme
2821 | BootDeviceType::NvmeViaScsi
2822 | BootDeviceType::NvmeViaNvme => true,
2823 }
2824 }
2825}
2826
2827impl Firmware {
2828 pub fn linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2830 use petri_artifacts_vmm_test::artifacts::loadable::*;
2831 match arch {
2832 MachineArch::X86_64 => Firmware::LinuxDirect {
2833 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_X64).erase(),
2834 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
2835 },
2836 MachineArch::Aarch64 => Firmware::LinuxDirect {
2837 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_AARCH64).erase(),
2838 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_AARCH64).erase(),
2839 },
2840 }
2841 }
2842
2843 pub fn linux_direct_bzimage(resolver: &ArtifactResolver<'_>) -> Self {
2848 use petri_artifacts_vmm_test::artifacts::loadable::*;
2849 Firmware::LinuxDirect {
2850 kernel: resolver.require(LINUX_DIRECT_TEST_BZIMAGE_X64).erase(),
2851 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
2852 }
2853 }
2854
2855 pub fn openhcl_linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2857 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2858 match arch {
2859 MachineArch::X86_64 => Firmware::OpenhclLinuxDirect {
2860 igvm_path: resolver.require(LATEST_LINUX_DIRECT_TEST_X64).erase(),
2861 openhcl_config: Default::default(),
2862 },
2863 MachineArch::Aarch64 => todo!("Linux direct not yet supported on aarch64"),
2864 }
2865 }
2866
2867 pub fn pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2869 use petri_artifacts_vmm_test::artifacts::loadable::*;
2870 Firmware::Pcat {
2871 guest,
2872 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2873 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2874 ide_controllers: [[None, None], [None, None]],
2875 }
2876 }
2877
2878 pub fn openhcl_pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2880 use petri_artifacts_vmm_test::artifacts::loadable::*;
2881 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2882 Firmware::OpenhclPcat {
2883 guest,
2884 igvm_path: resolver.require(LATEST_STANDARD_X64).erase(),
2885 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2886 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2887 openhcl_config: OpenHclConfig {
2888 vmbus_redirect: true,
2890 ..Default::default()
2891 },
2892 }
2893 }
2894
2895 pub fn uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch, guest: UefiGuest) -> Self {
2897 use petri_artifacts_vmm_test::artifacts::loadable::*;
2898 let uefi_firmware = match arch {
2899 MachineArch::X86_64 => resolver.require(UEFI_FIRMWARE_X64).erase(),
2900 MachineArch::Aarch64 => resolver.require(UEFI_FIRMWARE_AARCH64).erase(),
2901 };
2902 Firmware::Uefi {
2903 guest,
2904 uefi_firmware,
2905 uefi_config: Default::default(),
2906 }
2907 }
2908
2909 pub fn openhcl_uefi(
2911 resolver: &ArtifactResolver<'_>,
2912 arch: MachineArch,
2913 guest: UefiGuest,
2914 isolation: Option<IsolationType>,
2915 ) -> Self {
2916 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2917 let igvm_path = match arch {
2918 MachineArch::X86_64 if isolation.is_some() => resolver.require(LATEST_CVM_X64).erase(),
2919 MachineArch::X86_64 => resolver.require(LATEST_STANDARD_X64).erase(),
2920 MachineArch::Aarch64 => resolver.require(LATEST_STANDARD_AARCH64).erase(),
2921 };
2922 Firmware::OpenhclUefi {
2923 guest,
2924 isolation,
2925 igvm_path,
2926 uefi_config: Default::default(),
2927 openhcl_config: Default::default(),
2928 }
2929 }
2930
2931 fn is_openhcl(&self) -> bool {
2932 match self {
2933 Firmware::OpenhclLinuxDirect { .. }
2934 | Firmware::OpenhclUefi { .. }
2935 | Firmware::OpenhclPcat { .. } => true,
2936 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => false,
2937 }
2938 }
2939
2940 fn isolation(&self) -> Option<IsolationType> {
2941 match self {
2942 Firmware::OpenhclUefi { isolation, .. } => *isolation,
2943 Firmware::LinuxDirect { .. }
2944 | Firmware::Pcat { .. }
2945 | Firmware::Uefi { .. }
2946 | Firmware::OpenhclLinuxDirect { .. }
2947 | Firmware::OpenhclPcat { .. } => None,
2948 }
2949 }
2950
2951 fn is_linux_direct(&self) -> bool {
2952 match self {
2953 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => true,
2954 Firmware::Pcat { .. }
2955 | Firmware::Uefi { .. }
2956 | Firmware::OpenhclUefi { .. }
2957 | Firmware::OpenhclPcat { .. } => false,
2958 }
2959 }
2960
2961 pub fn linux_direct_initrd(&self) -> Option<&Path> {
2963 match self {
2964 Firmware::LinuxDirect { initrd, .. } => Some(initrd.get()),
2965 _ => None,
2966 }
2967 }
2968
2969 fn is_pcat(&self) -> bool {
2970 match self {
2971 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => true,
2972 Firmware::Uefi { .. }
2973 | Firmware::OpenhclUefi { .. }
2974 | Firmware::LinuxDirect { .. }
2975 | Firmware::OpenhclLinuxDirect { .. } => false,
2976 }
2977 }
2978
2979 fn os_flavor(&self) -> OsFlavor {
2980 match self {
2981 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => OsFlavor::Linux,
2982 Firmware::Uefi {
2983 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2984 ..
2985 }
2986 | Firmware::OpenhclUefi {
2987 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2988 ..
2989 } => OsFlavor::Uefi,
2990 Firmware::Pcat {
2991 guest: PcatGuest::Vhd(cfg),
2992 ..
2993 }
2994 | Firmware::OpenhclPcat {
2995 guest: PcatGuest::Vhd(cfg),
2996 ..
2997 }
2998 | Firmware::Uefi {
2999 guest: UefiGuest::Vhd(cfg),
3000 ..
3001 }
3002 | Firmware::OpenhclUefi {
3003 guest: UefiGuest::Vhd(cfg),
3004 ..
3005 } => cfg.os_flavor,
3006 Firmware::Pcat {
3007 guest: PcatGuest::Iso(cfg),
3008 ..
3009 }
3010 | Firmware::OpenhclPcat {
3011 guest: PcatGuest::Iso(cfg),
3012 ..
3013 } => cfg.os_flavor,
3014 }
3015 }
3016
3017 fn quirks(&self) -> GuestQuirks {
3018 match self {
3019 Firmware::Pcat {
3020 guest: PcatGuest::Vhd(cfg),
3021 ..
3022 }
3023 | Firmware::Uefi {
3024 guest: UefiGuest::Vhd(cfg),
3025 ..
3026 }
3027 | Firmware::OpenhclUefi {
3028 guest: UefiGuest::Vhd(cfg),
3029 ..
3030 } => cfg.quirks.clone(),
3031 Firmware::Pcat {
3032 guest: PcatGuest::Iso(cfg),
3033 ..
3034 } => cfg.quirks.clone(),
3035 _ => Default::default(),
3036 }
3037 }
3038
3039 fn expected_boot_event(&self) -> Option<FirmwareEvent> {
3040 match self {
3041 Firmware::LinuxDirect { .. }
3042 | Firmware::OpenhclLinuxDirect { .. }
3043 | Firmware::Uefi {
3044 guest: UefiGuest::GuestTestUefi(_),
3045 ..
3046 }
3047 | Firmware::OpenhclUefi {
3048 guest: UefiGuest::GuestTestUefi(_),
3049 ..
3050 } => None,
3051 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => {
3052 Some(FirmwareEvent::BootAttempt)
3054 }
3055 Firmware::Uefi {
3056 guest: UefiGuest::None,
3057 ..
3058 }
3059 | Firmware::OpenhclUefi {
3060 guest: UefiGuest::None,
3061 ..
3062 } => Some(FirmwareEvent::NoBootDevice),
3063 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => {
3064 Some(FirmwareEvent::BootSuccess)
3065 }
3066 }
3067 }
3068
3069 fn openhcl_config(&self) -> Option<&OpenHclConfig> {
3070 match self {
3071 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
3072 | Firmware::OpenhclUefi { openhcl_config, .. }
3073 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
3074 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
3075 }
3076 }
3077
3078 fn openhcl_config_mut(&mut self) -> Option<&mut OpenHclConfig> {
3079 match self {
3080 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
3081 | Firmware::OpenhclUefi { openhcl_config, .. }
3082 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
3083 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
3084 }
3085 }
3086
3087 #[cfg_attr(not(windows), expect(dead_code))]
3088 fn openhcl_firmware(&self) -> Option<&Path> {
3089 match self {
3090 Firmware::OpenhclLinuxDirect { igvm_path, .. }
3091 | Firmware::OpenhclUefi { igvm_path, .. }
3092 | Firmware::OpenhclPcat { igvm_path, .. } => Some(igvm_path.get()),
3093 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
3094 }
3095 }
3096
3097 fn into_runtime_config(
3098 self,
3099 vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
3100 ) -> PetriVmRuntimeConfig {
3101 match self {
3102 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
3103 | Firmware::OpenhclUefi { openhcl_config, .. }
3104 | Firmware::OpenhclPcat { openhcl_config, .. } => PetriVmRuntimeConfig {
3105 vtl2_settings: Some(
3106 openhcl_config
3107 .vtl2_settings
3108 .unwrap_or_else(default_vtl2_settings),
3109 ),
3110 ide_controllers: None,
3111 vmbus_storage_controllers,
3112 },
3113 Firmware::Pcat {
3114 ide_controllers, ..
3115 } => PetriVmRuntimeConfig {
3116 vtl2_settings: None,
3117 ide_controllers: Some(ide_controllers),
3118 vmbus_storage_controllers,
3119 },
3120 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } => PetriVmRuntimeConfig {
3121 vtl2_settings: None,
3122 ide_controllers: None,
3123 vmbus_storage_controllers,
3124 },
3125 }
3126 }
3127
3128 fn uefi_config(&self) -> Option<&UefiConfig> {
3129 match self {
3130 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
3131 Some(uefi_config)
3132 }
3133 Firmware::LinuxDirect { .. }
3134 | Firmware::OpenhclLinuxDirect { .. }
3135 | Firmware::Pcat { .. }
3136 | Firmware::OpenhclPcat { .. } => None,
3137 }
3138 }
3139
3140 fn uefi_config_mut(&mut self) -> Option<&mut UefiConfig> {
3141 match self {
3142 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
3143 Some(uefi_config)
3144 }
3145 Firmware::LinuxDirect { .. }
3146 | Firmware::OpenhclLinuxDirect { .. }
3147 | Firmware::Pcat { .. }
3148 | Firmware::OpenhclPcat { .. } => None,
3149 }
3150 }
3151
3152 fn boot_drive(&self) -> Option<Drive> {
3153 match self {
3154 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => None,
3155 Firmware::Pcat { guest, .. } | Firmware::OpenhclPcat { guest, .. } => {
3156 Some((guest.disk_path(), guest.is_dvd()))
3157 }
3158 Firmware::Uefi { guest, .. } | Firmware::OpenhclUefi { guest, .. } => {
3159 guest.disk_path().map(|dp| (dp, false))
3160 }
3161 }
3162 .map(|(disk_path, is_dvd)| Drive::new(Some(Disk::Differencing(disk_path)), is_dvd))
3163 }
3164
3165 fn vtl2_settings(&mut self) -> Option<&mut Vtl2Settings> {
3166 self.openhcl_config_mut()
3167 .map(|c| c.vtl2_settings.get_or_insert_with(default_vtl2_settings))
3168 }
3169
3170 fn ide_controllers(&self) -> Option<&[[Option<Drive>; 2]; 2]> {
3171 match self {
3172 Firmware::Pcat {
3173 ide_controllers, ..
3174 } => Some(ide_controllers),
3175 _ => None,
3176 }
3177 }
3178
3179 fn ide_controllers_mut(&mut self) -> Option<&mut [[Option<Drive>; 2]; 2]> {
3180 match self {
3181 Firmware::Pcat {
3182 ide_controllers, ..
3183 } => Some(ide_controllers),
3184 _ => None,
3185 }
3186 }
3187}
3188
3189#[derive(Debug)]
3192pub enum PcatGuest {
3193 Vhd(BootImageConfig<boot_image_type::Vhd>),
3195 Iso(BootImageConfig<boot_image_type::Iso>),
3197}
3198
3199impl PcatGuest {
3200 fn disk_path(&self) -> DiskPath {
3201 match self {
3202 PcatGuest::Vhd(disk) => disk.disk_path(),
3203 PcatGuest::Iso(disk) => disk.disk_path(),
3204 }
3205 }
3206
3207 fn is_dvd(&self) -> bool {
3208 matches!(self, Self::Iso(_))
3209 }
3210}
3211
3212#[derive(Debug)]
3215pub enum UefiGuest {
3216 Vhd(BootImageConfig<boot_image_type::Vhd>),
3218 GuestTestUefi(ResolvedArtifact),
3220 None,
3222}
3223
3224impl UefiGuest {
3225 pub fn guest_test_uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
3227 use petri_artifacts_vmm_test::artifacts::test_vhd::*;
3228 let artifact = match arch {
3229 MachineArch::X86_64 => resolver.require(GUEST_TEST_UEFI_X64).erase(),
3230 MachineArch::Aarch64 => resolver.require(GUEST_TEST_UEFI_AARCH64).erase(),
3231 };
3232 UefiGuest::GuestTestUefi(artifact)
3233 }
3234
3235 fn disk_path(&self) -> Option<DiskPath> {
3236 match self {
3237 UefiGuest::Vhd(vhd) => Some(vhd.disk_path()),
3238 UefiGuest::GuestTestUefi(p) => Some(DiskPath::Local(p.get().to_path_buf())),
3239 UefiGuest::None => None,
3240 }
3241 }
3242}
3243
3244pub mod boot_image_type {
3246 mod private {
3247 pub trait Sealed {}
3248 impl Sealed for super::Vhd {}
3249 impl Sealed for super::Iso {}
3250 }
3251
3252 pub trait BootImageType: private::Sealed {}
3255
3256 #[derive(Debug)]
3258 pub enum Vhd {}
3259
3260 #[derive(Debug)]
3262 pub enum Iso {}
3263
3264 impl BootImageType for Vhd {}
3265 impl BootImageType for Iso {}
3266}
3267
3268#[derive(Debug)]
3270pub struct BootImageConfig<T: boot_image_type::BootImageType> {
3271 artifact: ResolvedArtifactSource,
3273 os_flavor: OsFlavor,
3275 quirks: GuestQuirks,
3279 _type: core::marker::PhantomData<T>,
3281}
3282
3283impl<T: boot_image_type::BootImageType> BootImageConfig<T> {
3284 fn disk_path(&self) -> DiskPath {
3286 match self.artifact.get() {
3287 ArtifactSource::Local(p) => DiskPath::Local(p.clone()),
3288 ArtifactSource::Remote { url } => DiskPath::Remote { url: url.clone() },
3289 }
3290 }
3291}
3292
3293impl BootImageConfig<boot_image_type::Vhd> {
3294 pub fn from_vhd<A>(artifact: ResolvedArtifactSource<A>) -> Self
3296 where
3297 A: petri_artifacts_common::tags::IsTestVhd,
3298 {
3299 BootImageConfig {
3300 artifact: artifact.erase(),
3301 os_flavor: A::OS_FLAVOR,
3302 quirks: A::quirks(),
3303 _type: std::marker::PhantomData,
3304 }
3305 }
3306}
3307
3308impl BootImageConfig<boot_image_type::Iso> {
3309 pub fn from_iso<A>(artifact: ResolvedArtifactSource<A>) -> Self
3311 where
3312 A: petri_artifacts_common::tags::IsTestIso,
3313 {
3314 BootImageConfig {
3315 artifact: artifact.erase(),
3316 os_flavor: A::OS_FLAVOR,
3317 quirks: A::quirks(),
3318 _type: std::marker::PhantomData,
3319 }
3320 }
3321}
3322
3323#[derive(Debug, Clone, Copy)]
3325pub enum IsolationType {
3326 Vbs,
3328 Snp,
3330 Tdx,
3332}
3333
3334#[derive(Debug, Clone, Copy)]
3336pub struct OpenHclServicingFlags {
3337 pub enable_nvme_keepalive: bool,
3340 pub enable_mana_keepalive: bool,
3342 pub override_version_checks: bool,
3344 pub stop_timeout_hint_secs: Option<u16>,
3346}
3347
3348#[derive(Debug, Clone)]
3350pub enum DiskPath {
3351 Local(PathBuf),
3353 Remote {
3355 url: String,
3357 },
3358}
3359
3360impl From<PathBuf> for DiskPath {
3361 fn from(path: PathBuf) -> Self {
3362 DiskPath::Local(path)
3363 }
3364}
3365
3366#[derive(Debug, Clone)]
3368pub enum Disk {
3369 Memory(u64),
3371 Differencing(DiskPath),
3373 Persistent(PathBuf),
3375 Temporary(Arc<TempPath>),
3377}
3378
3379#[derive(Debug, Clone)]
3381pub struct PetriVmgsDisk {
3382 pub disk: Disk,
3384 pub encryption_policy: GuestStateEncryptionPolicy,
3386}
3387
3388impl Default for PetriVmgsDisk {
3389 fn default() -> Self {
3390 PetriVmgsDisk {
3391 disk: Disk::Memory(vmgs_format::VMGS_DEFAULT_CAPACITY),
3392 encryption_policy: GuestStateEncryptionPolicy::None(false),
3394 }
3395 }
3396}
3397
3398#[derive(Debug, Clone)]
3400pub enum PetriVmgsResource {
3401 Disk(PetriVmgsDisk),
3403 ReprovisionOnFailure(PetriVmgsDisk),
3405 Reprovision(PetriVmgsDisk),
3407 Ephemeral,
3409}
3410
3411impl PetriVmgsResource {
3412 pub fn vmgs(&self) -> Option<&PetriVmgsDisk> {
3414 match self {
3415 PetriVmgsResource::Disk(vmgs)
3416 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
3417 | PetriVmgsResource::Reprovision(vmgs) => Some(vmgs),
3418 PetriVmgsResource::Ephemeral => None,
3419 }
3420 }
3421
3422 pub fn disk(&self) -> Option<&Disk> {
3424 self.vmgs().map(|vmgs| &vmgs.disk)
3425 }
3426
3427 pub fn encryption_policy(&self) -> Option<GuestStateEncryptionPolicy> {
3429 self.vmgs().map(|vmgs| vmgs.encryption_policy)
3430 }
3431}
3432
3433#[derive(Debug, Clone, Copy)]
3435pub enum PetriGuestStateLifetime {
3436 Disk,
3439 ReprovisionOnFailure,
3441 Reprovision,
3443 Ephemeral,
3445}
3446
3447#[derive(Debug, Clone, Copy)]
3449pub enum SecureBootTemplate {
3450 MicrosoftWindows,
3452 MicrosoftUefiCertificateAuthority,
3454}
3455
3456#[derive(Default, Debug, Clone)]
3459pub struct VmmQuirks {
3460 pub flaky_boot: Option<Duration>,
3463}
3464
3465fn make_vm_safe_name(name: &str) -> String {
3471 const MAX_VM_NAME_LENGTH: usize = 100;
3472 const HASH_LENGTH: usize = 4;
3473 const MAX_PREFIX_LENGTH: usize = MAX_VM_NAME_LENGTH - HASH_LENGTH;
3474
3475 if name.len() <= MAX_VM_NAME_LENGTH {
3476 name.to_owned()
3477 } else {
3478 let mut hasher = DefaultHasher::new();
3480 name.hash(&mut hasher);
3481 let hash = hasher.finish();
3482
3483 let hash_suffix = format!("{:04x}", hash & 0xFFFF);
3485
3486 let truncated = &name[..MAX_PREFIX_LENGTH];
3488 tracing::debug!(
3489 "VM name too long ({}), truncating '{}' to '{}{}'",
3490 name.len(),
3491 name,
3492 truncated,
3493 hash_suffix
3494 );
3495
3496 format!("{}{}", truncated, hash_suffix)
3497 }
3498}
3499
3500#[derive(Debug, Clone, Copy, Eq, PartialEq)]
3502pub enum PetriHaltReason {
3503 PowerOff,
3505 Reset,
3507 Hibernate,
3509 TripleFault,
3511 Other,
3513}
3514
3515impl PetriHaltReason {
3516 pub fn with_detail(self, detail: String) -> PetriHaltReasonDetail {
3518 PetriHaltReasonDetail {
3519 reason: self,
3520 detail,
3521 }
3522 }
3523}
3524
3525#[derive(Debug, Clone)]
3527pub struct PetriHaltReasonDetail {
3528 pub reason: PetriHaltReason,
3530 pub detail: String,
3532}
3533
3534fn append_cmdline(cmd: &mut Option<String>, add_cmd: impl AsRef<str>) {
3535 if let Some(cmd) = cmd.as_mut() {
3536 cmd.push(' ');
3537 cmd.push_str(add_cmd.as_ref());
3538 } else {
3539 *cmd = Some(add_cmd.as_ref().to_string());
3540 }
3541}
3542
3543async fn save_inspect(
3544 name: &str,
3545 inspect: std::pin::Pin<Box<dyn Future<Output = anyhow::Result<inspect::Node>> + Send>>,
3546 log_source: &PetriLogSource,
3547) {
3548 tracing::info!("Collecting {name} inspect details.");
3549 let node = match inspect.await {
3550 Ok(n) => n,
3551 Err(e) => {
3552 tracing::error!(?e, "Failed to get {name}");
3553 return;
3554 }
3555 };
3556 if let Err(e) = log_source.write_attachment(
3557 &format!("timeout_inspect_{name}.log"),
3558 format!("{node:#}").as_bytes(),
3559 ) {
3560 tracing::error!(?e, "Failed to save {name} inspect log");
3561 return;
3562 }
3563 tracing::info!("{name} inspect task finished.");
3564}
3565
3566pub struct ModifyFn<T>(pub Box<dyn FnOnce(T) -> T + Send>);
3568
3569impl<T> Debug for ModifyFn<T> {
3570 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3571 write!(f, "_")
3572 }
3573}
3574
3575fn default_vtl2_settings() -> Vtl2Settings {
3577 Vtl2Settings {
3578 version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
3579 fixed: None,
3580 dynamic: Some(Default::default()),
3581 namespace_settings: Default::default(),
3582 }
3583}
3584
3585#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3587pub enum Vtl {
3588 Vtl0 = 0,
3590 Vtl1 = 1,
3592 Vtl2 = 2,
3594}
3595
3596#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3598pub enum VmbusStorageType {
3599 Scsi,
3601 Nvme,
3603 VirtioBlk,
3605}
3606
3607#[derive(Debug, Clone)]
3609pub struct Drive {
3610 pub disk: Option<Disk>,
3612 pub is_dvd: bool,
3614}
3615
3616impl Drive {
3617 pub fn new(disk: Option<Disk>, is_dvd: bool) -> Self {
3619 Self { disk, is_dvd }
3620 }
3621}
3622
3623#[derive(Debug, Clone)]
3625pub struct VmbusStorageController {
3626 pub target_vtl: Vtl,
3628 pub controller_type: VmbusStorageType,
3630 pub drives: HashMap<u32, Drive>,
3632}
3633
3634impl VmbusStorageController {
3635 pub fn new(target_vtl: Vtl, controller_type: VmbusStorageType) -> Self {
3637 Self {
3638 target_vtl,
3639 controller_type,
3640 drives: HashMap::new(),
3641 }
3642 }
3643
3644 pub fn set_drive(
3646 &mut self,
3647 lun: Option<u32>,
3648 drive: Drive,
3649 allow_modify_existing: bool,
3650 ) -> u32 {
3651 let lun = lun.unwrap_or_else(|| {
3652 let mut lun = None;
3654 for x in 0..u8::MAX as u32 {
3655 if !self.drives.contains_key(&x) {
3656 lun = Some(x);
3657 break;
3658 }
3659 }
3660 lun.expect("all locations on this controller are in use")
3661 });
3662
3663 if self.drives.insert(lun, drive).is_some() && !allow_modify_existing {
3664 panic!("a disk with lun {lun} already existed on this controller");
3665 }
3666
3667 lun
3668 }
3669}
3670
3671pub(crate) fn petri_disk_cache_dir() -> String {
3673 if let Ok(dir) = std::env::var("PETRI_CACHE_DIR") {
3674 return dir;
3675 }
3676
3677 #[cfg(target_os = "macos")]
3678 {
3679 if let Ok(home) = std::env::var("HOME") {
3680 return format!("{home}/Library/Caches/petri");
3681 }
3682 }
3683
3684 #[cfg(windows)]
3685 {
3686 if let Ok(local) = std::env::var("LOCALAPPDATA") {
3687 return format!("{local}\\petri\\cache");
3688 }
3689 }
3690
3691 if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
3693 return format!("{xdg}/petri");
3694 }
3695 if let Ok(home) = std::env::var("HOME") {
3696 return format!("{home}/.cache/petri");
3697 }
3698
3699 ".cache/petri".to_string()
3700}
3701
3702#[cfg(test)]
3703mod tests {
3704 use super::make_vm_safe_name;
3705 use crate::Drive;
3706 use crate::VmbusStorageController;
3707 use crate::VmbusStorageType;
3708 use crate::Vtl;
3709
3710 #[test]
3711 fn test_short_names_unchanged() {
3712 let short_name = "short_test_name";
3713 assert_eq!(make_vm_safe_name(short_name), short_name);
3714 }
3715
3716 #[test]
3717 fn test_exactly_100_chars_unchanged() {
3718 let name_100 = "a".repeat(100);
3719 assert_eq!(make_vm_safe_name(&name_100), name_100);
3720 }
3721
3722 #[test]
3723 fn test_long_name_truncated() {
3724 let long_name = "multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_openhcl_servicing";
3725 let result = make_vm_safe_name(long_name);
3726
3727 assert_eq!(result.len(), 100);
3729
3730 assert!(result.starts_with("multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_ope"));
3732
3733 let suffix = &result[96..];
3735 assert_eq!(suffix.len(), 4);
3736 assert!(u16::from_str_radix(suffix, 16).is_ok());
3738 }
3739
3740 #[test]
3741 fn test_deterministic_results() {
3742 let long_name = "very_long_test_name_that_exceeds_the_100_character_limit_and_should_be_truncated_consistently_every_time";
3743 let result1 = make_vm_safe_name(long_name);
3744 let result2 = make_vm_safe_name(long_name);
3745
3746 assert_eq!(result1, result2);
3747 assert_eq!(result1.len(), 100);
3748 }
3749
3750 #[test]
3751 fn test_different_names_different_hashes() {
3752 let name1 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_1";
3753 let name2 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_2";
3754
3755 let result1 = make_vm_safe_name(name1);
3756 let result2 = make_vm_safe_name(name2);
3757
3758 assert_eq!(result1.len(), 100);
3760 assert_eq!(result2.len(), 100);
3761
3762 assert_ne!(result1, result2);
3764 assert_ne!(&result1[96..], &result2[96..]);
3765 }
3766
3767 #[test]
3768 fn test_vmbus_storage_controller() {
3769 let mut controller = VmbusStorageController::new(Vtl::Vtl0, VmbusStorageType::Scsi);
3770 assert_eq!(
3771 controller.set_drive(Some(1), Drive::new(None, false), false),
3772 1
3773 );
3774 assert!(controller.drives.contains_key(&1));
3775 assert_eq!(
3776 controller.set_drive(None, Drive::new(None, false), false),
3777 0
3778 );
3779 assert!(controller.drives.contains_key(&0));
3780 assert_eq!(
3781 controller.set_drive(None, Drive::new(None, false), false),
3782 2
3783 );
3784 assert!(controller.drives.contains_key(&2));
3785 assert_eq!(
3786 controller.set_drive(Some(0), Drive::new(None, false), true),
3787 0
3788 );
3789 assert!(controller.drives.contains_key(&0));
3790 }
3791}