1#[cfg(windows)]
6pub mod hyperv;
7pub mod openvmm;
9pub mod qemu;
11pub mod vtl2_settings;
12
13use crate::PetriLogSource;
14use crate::PetriTestParams;
15use crate::ShutdownKind;
16use crate::disk_image::AgentImage;
17use crate::disk_image::ImageEntry;
18use crate::disk_image::SECTOR_SIZE;
19use crate::mshv_boot::MshvBoot;
20use crate::openhcl_diag::OpenHclDiagHandler;
21use crate::test::PetriPostTestHook;
22use crate::vtl2_settings::ControllerType;
23use crate::vtl2_settings::Vtl2LunBuilder;
24use crate::vtl2_settings::Vtl2StorageBackingDeviceBuilder;
25use crate::vtl2_settings::Vtl2StorageControllerBuilder;
26use anyhow::Context;
27use async_trait::async_trait;
28use futures::FutureExt as _;
29use get_resources::ged::FirmwareEvent;
30use guid::Guid;
31use mesh::CancelContext;
32use openvmm_defs::config::Vtl2BaseAddressType;
33use pal_async::DefaultDriver;
34use pal_async::task::Spawn;
35use pal_async::task::Task;
36use pal_async::timer::PolledTimer;
37use petri_artifact_resolver_openvmm_known_paths::resolve_artifact;
38use petri_artifacts_common::tags::GuestQuirks;
39use petri_artifacts_common::tags::GuestQuirksInner;
40use petri_artifacts_common::tags::InitialRebootCondition;
41use petri_artifacts_common::tags::IsLoadable;
42use petri_artifacts_common::tags::IsOpenhclIgvm;
43use petri_artifacts_common::tags::IsTestVmgs;
44use petri_artifacts_common::tags::MachineArch;
45use petri_artifacts_common::tags::OsFlavor;
46use petri_artifacts_core::ArtifactHandle;
47use petri_artifacts_core::ArtifactId;
48use petri_artifacts_core::ArtifactResolver;
49use petri_artifacts_core::ArtifactSource;
50use petri_artifacts_core::ErasedArtifactHandle;
51use petri_artifacts_core::ResolvedArtifact;
52use petri_artifacts_core::ResolvedArtifactSource;
53use petri_artifacts_core::ResolvedOptionalArtifact;
54use petri_artifacts_core::env::PETRI_REMOTE_ARTIFACTS;
55use petri_artifacts_core::tags::IsNextestArchive;
56use petri_artifacts_vmm_test::env::*;
57use pipette_client::PipetteClient;
58use std::collections::BTreeMap;
59use std::collections::HashMap;
60use std::fmt::Debug;
61use std::io::Write;
62use std::panic::AssertUnwindSafe;
63use std::path::Path;
64use std::path::PathBuf;
65use std::path::absolute;
66use std::sync::Arc;
67use std::time::Duration;
68use tempfile::TempPath;
69use vmgs_resources::GuestStateEncryptionPolicy;
70use vtl2_settings_proto::StorageController;
71use vtl2_settings_proto::Vtl2Settings;
72
73pub struct PetriVmArtifacts<T: PetriVmmBackend> {
76 pub backend: T,
78 pub firmware: Firmware,
80 pub arch: MachineArch,
82 pub agent_image: Option<AgentImage>,
84 pub openhcl_agent_image: Option<AgentImage>,
86 pub pipette_binary: Option<ResolvedArtifact>,
88 pub nested_test: Option<NestedTestDeps>,
90}
91
92impl<T: PetriVmmBackend> PetriVmArtifacts<T> {
93 pub fn new(
97 resolver: &ArtifactResolver<'_>,
98 firmware: Firmware,
99 arch: MachineArch,
100 with_vtl0_pipette: bool,
101 ) -> Option<Self> {
102 Self::new_maybe_nested(resolver, firmware, arch, with_vtl0_pipette, None)
103 }
104
105 pub fn new_maybe_nested(
109 resolver: &ArtifactResolver<'_>,
110 firmware: Firmware,
111 arch: MachineArch,
112 with_vtl0_pipette: bool,
113 nested_test: Option<NestedTestDeps>,
114 ) -> Option<Self> {
115 let different_nested_target = nested_test
119 .as_ref()
120 .map(|t| t.archive.target_triple().unwrap())
121 .is_some_and(|t| {
122 let host_target = target_lexicon::Triple::host();
123 host_target.operating_system != t.operating_system
124 });
125
126 if !(T::SUPPORTS_CPU_EMULATION || arch == MachineArch::host())
127 || !T::check_compat(&firmware, arch)
128 || different_nested_target && (cfg!(windows) || !T::supports_file_sharing(&firmware))
129 {
130 return None;
131 }
132
133 let pipette_binary = if with_vtl0_pipette {
134 Some(Self::resolve_pipette_binary(
135 resolver,
136 firmware.os_flavor(),
137 arch,
138 ))
139 } else {
140 None
141 };
142
143 Some(Self {
144 backend: T::new(resolver, arch),
145 arch,
146 agent_image: (!firmware.is_isolated_without_paravisor()).then(|| {
147 if with_vtl0_pipette {
148 AgentImage::new(firmware.os_flavor()).with_pipette(resolver, arch)
149 } else {
150 AgentImage::new(firmware.os_flavor())
151 }
152 }),
153 openhcl_agent_image: if firmware.is_openhcl() {
154 Some(AgentImage::new(OsFlavor::Linux).with_pipette(resolver, arch))
155 } else {
156 None
157 },
158 pipette_binary,
159 firmware,
160 nested_test,
161 })
162 }
163
164 fn resolve_pipette_binary(
165 resolver: &ArtifactResolver<'_>,
166 os_flavor: OsFlavor,
167 arch: MachineArch,
168 ) -> ResolvedArtifact {
169 use petri_artifacts_common::artifacts as common_artifacts;
170 match (os_flavor, arch) {
171 (OsFlavor::Linux, MachineArch::X86_64) => resolver
172 .require(common_artifacts::PIPETTE_LINUX_X64_MUSL)
173 .erase(),
174 (OsFlavor::Linux, MachineArch::Aarch64) => resolver
175 .require(common_artifacts::PIPETTE_LINUX_AARCH64_MUSL)
176 .erase(),
177 (OsFlavor::Windows, MachineArch::X86_64) => resolver
178 .require(common_artifacts::PIPETTE_WINDOWS_X64)
179 .erase(),
180 (OsFlavor::Windows, MachineArch::Aarch64) => resolver
181 .require(common_artifacts::PIPETTE_WINDOWS_AARCH64)
182 .erase(),
183 (OsFlavor::FreeBsd | OsFlavor::Uefi, _) => {
184 panic!("No pipette binary for this OS flavor")
185 }
186 }
187 }
188}
189
190pub struct PetriVmBuilder<T: PetriVmmBackend> {
192 backend: T,
194 config: PetriVmConfig,
196 modify_vmm_config: Option<ModifyFn<T::VmmConfig>>,
198 resources: PetriVmResources,
200
201 guest_quirks: GuestQuirksInner,
203 vmm_quirks: VmmQuirks,
204
205 expected_boot_event: Option<FirmwareEvent>,
208 override_expect_reset: bool,
209
210 agent_image: Option<AgentImage>,
214 openhcl_agent_image: Option<AgentImage>,
216 boot_device_type: BootDeviceType,
218 pcie_boot_port: Option<String>,
221
222 minimal_mode: bool,
224 pipette_binary: Option<ResolvedArtifact>,
226 enable_serial: bool,
228 enable_screenshots: bool,
230 use_virtio_vsock: bool,
232 #[cfg(target_os = "linux")]
234 vhost_vsock_guest_cid: Option<u32>,
235 no_vmbus: bool,
237 no_hv: bool,
239 capture_inspect_on_failure: bool,
241 watchdog_timeout: Option<Duration>,
242 external_pipette: bool,
243
244 nested_test: Option<ResolvedNestedTestDeps>,
245}
246
247const INSPECT_TIMEOUT: Duration = Duration::from_secs(10);
249
250impl<T: PetriVmmBackend> Debug for PetriVmBuilder<T> {
251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 f.debug_struct("PetriVmBuilder")
253 .field("backend", &self.backend)
254 .field("config", &self.config)
255 .field("modify_vmm_config", &self.modify_vmm_config.is_some())
256 .field("resources", &self.resources)
257 .field("guest_quirks", &self.guest_quirks)
258 .field("vmm_quirks", &self.vmm_quirks)
259 .field("expected_boot_event", &self.expected_boot_event)
260 .field("override_expect_reset", &self.override_expect_reset)
261 .field("agent_image", &self.agent_image)
262 .field("openhcl_agent_image", &self.openhcl_agent_image)
263 .field("boot_device_type", &self.boot_device_type)
264 .field("pcie_boot_port", &self.pcie_boot_port)
265 .field("minimal_mode", &self.minimal_mode)
266 .field("enable_serial", &self.enable_serial)
267 .field("enable_screenshots", &self.enable_screenshots)
268 .field("use_virtio_vsock", &self.use_virtio_vsock)
269 .field("no_vmbus", &self.no_vmbus)
270 .field("no_hv", &self.no_hv)
271 .finish()
272 }
273}
274
275#[derive(Debug)]
277pub struct PetriVmConfig {
278 pub name: String,
280 pub arch: MachineArch,
282 pub host_log_levels: Option<OpenvmmLogConfig>,
284 pub firmware: Firmware,
286 pub hibernation_enabled: bool,
288 pub ipmi_enabled: bool,
290 pub memory: MemoryConfig,
292 pub proc_topology: ProcessorTopology,
294 pub nested_virt_enabled: bool,
296 pub vmgs: PetriVmgsResource,
298 pub tpm: Option<TpmConfig>,
300 pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
302 pub pcie_nvme_drives: Vec<PcieNvmeDrive>,
304 pub pcie_virtio_blk_drives: Vec<PcieVirtioBlkDrive>,
306 pub physical_nvme_devices: HashMap<Guid, PhysicalNvmeDevice>,
308 pub guest_shares: HashMap<String, GuestShare>,
310}
311
312#[derive(Debug, Clone)]
314pub struct GuestShare {
315 pub host_path: PathBuf,
317 pub guest_path: String,
319}
320
321#[derive(Debug)]
323pub struct PcieNvmeDrive {
324 pub port_name: String,
326 pub nsid: u32,
328 pub drive: Drive,
330}
331
332#[derive(Debug)]
334pub struct PcieVirtioBlkDrive {
335 pub port_name: String,
337 pub drive: Drive,
339}
340
341#[derive(Debug, Clone)]
344pub struct PhysicalNvmeDevice {
345 pub target_vtl: Vtl,
347 pub nsid: u32,
349 pub namespace_size_mib: u64,
351}
352
353#[derive(Clone)]
356pub struct PetriVmProperties {
357 pub is_openhcl: bool,
359 pub is_isolated: bool,
361 pub is_pcat: bool,
363 pub is_linux_direct: bool,
365 pub using_vtl0_pipette: bool,
367 pub external_pipette: bool,
369 pub using_vpci: bool,
371 pub os_flavor: OsFlavor,
373 pub minimal_mode: bool,
375 pub uses_pipette_as_init: bool,
377 pub enable_serial: bool,
379 pub has_agent_disk: bool,
381 pub use_virtio_vsock: bool,
383 #[cfg(target_os = "linux")]
385 pub vhost_vsock_guest_cid: Option<u32>,
386 pub no_vmbus: bool,
388 pub no_hv: bool,
390}
391
392pub struct PetriVmRuntimeConfig {
394 pub vtl2_settings: Option<Vtl2Settings>,
396 pub ide_controllers: Option<[[Option<Drive>; 2]; 2]>,
398 pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
400}
401
402#[derive(Debug)]
404pub struct PetriVmResources {
405 pub driver: DefaultDriver,
407 pub log_source: PetriLogSource,
409 pub prebuilt_initrd: Option<PetriInitrd>,
411}
412
413#[async_trait]
415pub trait PetriVmmBackend: Debug {
416 type VmmConfig: 'static;
418
419 type VmRuntime: PetriVmRuntime;
421
422 const SUPPORTS_VMBUS: bool;
424
425 const SUPPORTS_CPU_EMULATION: bool = false;
430
431 const SUPPORTS_FILE_SHARING: bool = false;
433
434 fn supports_file_sharing(firmware: &Firmware) -> bool {
436 Self::SUPPORTS_FILE_SHARING && firmware.os_flavor() == OsFlavor::Linux
437 }
438
439 fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool;
448
449 fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks);
451
452 fn default_servicing_flags() -> OpenHclServicingFlags;
454
455 fn create_guest_dump_disk() -> anyhow::Result<
458 Option<(
459 Arc<TempPath>,
460 Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
461 )>,
462 >;
463
464 fn build_custom_init_script(pipette_path: &str) -> Option<String>;
467
468 fn new(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self;
473
474 async fn run(
476 self,
477 config: PetriVmConfig,
478 modify_vmm_config: Option<ModifyFn<Self::VmmConfig>>,
479 resources: &PetriVmResources,
480 properties: &PetriVmProperties,
481 ) -> anyhow::Result<(Self::VmRuntime, PetriVmRuntimeConfig)>;
482}
483
484pub(crate) const PETRI_IDE_BOOT_CONTROLLER_NUMBER: u32 = 0;
486pub(crate) const PETRI_IDE_BOOT_LUN: u8 = 0;
487pub(crate) const PETRI_IDE_BOOT_CONTROLLER: Guid =
488 guid::guid!("ca56751f-e643-4bef-bf54-f73678e8b7b5");
489
490pub(crate) const PETRI_SCSI_BOOT_LUN: u32 = 0;
492pub(crate) const PETRI_SCSI_PIPETTE_LUN: u32 = 1;
493pub(crate) const PETRI_SCSI_CRASH_LUN: u32 = 2;
494pub(crate) const PETRI_SCSI_VTL0_CONTROLLER: Guid =
496 guid::guid!("27b553e8-8b39-411b-a55f-839971a7884f");
497pub(crate) const PETRI_SCSI_VTL2_CONTROLLER: Guid =
499 guid::guid!("766e96f8-2ceb-437e-afe3-a93169e48a7c");
500pub(crate) const PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER: Guid =
502 guid::guid!("6c474f47-ed39-49e6-bbb9-142177a1da6e");
503
504pub(crate) const PETRI_NVME_BOOT_NSID: u32 = 37;
506pub(crate) const PETRI_NVME_BOOT_VTL0_CONTROLLER: Guid =
508 guid::guid!("e23a04e2-90f5-4852-bc9d-e7ac691b756c");
509pub(crate) const PETRI_NVME_BOOT_VTL2_CONTROLLER: Guid =
511 guid::guid!("92bc8346-718b-449a-8751-edbf3dcd27e4");
512
513pub(crate) const PETRI_PCIE_NVME_AGENT_PORT: &str = "s0rc0rp1";
515pub(crate) const PETRI_PCIE_NVME_AGENT_NSID: u32 = 1;
517
518const NESTED_TEST_CONTENT_DIR: &str = "vmm_tests";
519const NESTED_IMAGES_DIR: &str = "images";
520const NESTED_RESULTS_DIR: &str = "test_results";
521const NESTED_TEST_BINARY: &str = "nested_test";
522const NESTED_TEMP_DIR: &str = "temp";
523const NESTED_CONTENT_TAG: &str = "testcontent";
524const NESTED_IMAGES_TAG: &str = "testimages";
525
526pub struct PetriVm<T: PetriVmmBackend> {
528 resources: PetriVmResources,
529 runtime: PetriVmRuntimeGuard<T::VmRuntime>,
530 watchdog_tasks: Vec<Task<()>>,
531 openhcl_diag_handler: Option<OpenHclDiagHandler>,
532
533 arch: MachineArch,
534 guest_quirks: GuestQuirksInner,
535 vmm_quirks: VmmQuirks,
536 expected_boot_event: Option<FirmwareEvent>,
537
538 config: PetriVmRuntimeConfig,
539 properties: PetriVmProperties,
540
541 nested_test: Option<ResolvedNestedTestDeps>,
542 guest_shares: HashMap<String, GuestShare>,
543 name: String,
544}
545
546struct PetriVmRuntimeGuard<T: PetriVmRuntime> {
550 runtime: Option<T>,
551 driver: DefaultDriver,
552 log_source: PetriLogSource,
553 capture_inspect_on_drop: bool,
554}
555
556impl<T: PetriVmRuntime> PetriVmRuntimeGuard<T> {
557 fn take_for_teardown(&mut self) -> T {
558 self.runtime.take().expect("runtime has already been taken")
559 }
560}
561
562impl<T: PetriVmRuntime> std::ops::Deref for PetriVmRuntimeGuard<T> {
563 type Target = T;
564
565 fn deref(&self) -> &T {
566 self.runtime
567 .as_ref()
568 .expect("runtime has already been taken")
569 }
570}
571
572impl<T: PetriVmRuntime> std::ops::DerefMut for PetriVmRuntimeGuard<T> {
573 fn deref_mut(&mut self) -> &mut T {
574 self.runtime
575 .as_mut()
576 .expect("runtime has already been taken")
577 }
578}
579
580impl<T: PetriVmRuntime> Drop for PetriVmRuntimeGuard<T> {
581 fn drop(&mut self) {
582 let Some(runtime) = self.runtime.take() else {
583 return;
584 };
585 if !self.capture_inspect_on_drop {
586 return;
587 }
588 let inspector = runtime.inspector();
589 let openhcl_diag_handler = runtime.openhcl_diag();
590 if inspector.is_none() && openhcl_diag_handler.is_none() {
591 return;
592 }
593 let log_source = self.log_source.clone();
594
595 let capture = async move {
596 let vmm = async {
597 if let Some(inspector) = &inspector {
598 collect_inspect("vmm", inspector.inspect(""), &log_source, "failure").await;
599 }
600 };
601 let openhcl = async {
602 if let Some(diag) = &openhcl_diag_handler {
603 collect_inspect(
604 "openhcl",
605 diag.inspect("", None, None),
606 &log_source,
607 "failure",
608 )
609 .await;
610 }
611 };
612 futures::future::join(vmm, openhcl).await;
613 drop(runtime);
614 };
615
616 self.driver
620 .spawn("petri-inspect-on-drop", async move {
621 if let Err(e) = AssertUnwindSafe(capture).catch_unwind().await {
624 tracing::error!(?e, "panicked while collecting inspect state");
625 }
626 })
627 .detach();
628 }
629}
630
631impl<T: PetriVmmBackend> PetriVmBuilder<T> {
632 pub fn new(
634 params: PetriTestParams<'_>,
635 artifacts: PetriVmArtifacts<T>,
636 driver: &DefaultDriver,
637 ) -> anyhow::Result<Self> {
638 let isolated_without_paravisor = artifacts.firmware.is_isolated_without_paravisor();
639 let builder = Self::minimal(params.test_name, params.log_source, artifacts, driver)?
640 .clear_minimal_mode()
641 .with_serial_output()
642 .with_capture_inspect_on_failure();
643 if isolated_without_paravisor {
644 Ok(builder)
645 } else {
646 builder
647 .add_petri_scsi_controllers()
648 .add_guest_crash_disk(params.post_test_hooks)
649 }
650 }
651
652 pub fn minimal(
663 test_name: &str,
664 log_source: &PetriLogSource,
665 artifacts: PetriVmArtifacts<T>,
666 driver: &DefaultDriver,
667 ) -> anyhow::Result<Self> {
668 let isolated_without_paravisor = artifacts.firmware.is_isolated_without_paravisor();
669 match &artifacts.firmware {
670 Firmware::Uefi {
671 guest: UefiGuest::Mshv(_),
672 ..
673 } => anyhow::ensure!(
674 artifacts.arch == MachineArch::X86_64,
675 "mshv boot requires x86-64 UEFI"
676 ),
677 Firmware::OpenhclUefi {
678 guest: UefiGuest::Mshv(_),
679 ..
680 } => anyhow::bail!("mshv boot with OpenHCL is not supported"),
681 _ => {}
682 }
683 let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
684 let expected_boot_event = artifacts.firmware.expected_boot_event();
685 let boot_device_type = match artifacts.firmware {
686 Firmware::LinuxDirect { .. } | Firmware::IgvmLinuxDirect { .. } => BootDeviceType::None,
687 Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
688 Firmware::Pcat { .. } => BootDeviceType::Ide,
689 Firmware::OpenhclPcat { .. } => BootDeviceType::IdeViaScsi,
690 Firmware::Uefi {
691 guest: UefiGuest::None,
692 ..
693 }
694 | Firmware::OpenhclUefi {
695 guest: UefiGuest::None,
696 ..
697 } => BootDeviceType::None,
698 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
699 };
700
701 let supports_file_sharing = T::supports_file_sharing(&artifacts.firmware);
702 let external_pipette = isolated_without_paravisor && artifacts.pipette_binary.is_some();
703 let builder = Self {
704 backend: artifacts.backend,
705 config: PetriVmConfig {
706 name: test_name.to_string(),
707 arch: artifacts.arch,
708 host_log_levels: None,
709 firmware: artifacts.firmware,
710 hibernation_enabled: false,
711 ipmi_enabled: false,
712 memory: Default::default(),
713 proc_topology: Default::default(),
714 nested_virt_enabled: false,
715
716 vmgs: PetriVmgsResource::Ephemeral,
717 tpm: None,
718 vmbus_storage_controllers: HashMap::new(),
719 pcie_nvme_drives: Vec::new(),
720 pcie_virtio_blk_drives: Vec::new(),
721 physical_nvme_devices: HashMap::new(),
722 guest_shares: HashMap::new(),
723 },
724 modify_vmm_config: None,
725 resources: PetriVmResources {
726 driver: driver.clone(),
727 log_source: log_source.clone(),
728 prebuilt_initrd: None,
729 },
730
731 guest_quirks,
732 vmm_quirks,
733 expected_boot_event,
734 override_expect_reset: false,
735
736 agent_image: artifacts.agent_image,
737 openhcl_agent_image: artifacts.openhcl_agent_image,
738 boot_device_type,
739 pcie_boot_port: None,
740
741 minimal_mode: true,
742 pipette_binary: artifacts.pipette_binary,
743 enable_serial: false,
744 enable_screenshots: true,
745 use_virtio_vsock: isolated_without_paravisor,
746 #[cfg(target_os = "linux")]
747 vhost_vsock_guest_cid: None,
748 no_vmbus: !T::SUPPORTS_VMBUS || isolated_without_paravisor,
749 no_hv: false,
750 capture_inspect_on_failure: false,
751 watchdog_timeout: Some(Duration::from_secs(600)),
752 external_pipette: false,
753
754 nested_test: artifacts
755 .nested_test
756 .map(|t| t.resolve(!supports_file_sharing))
757 .transpose()?,
758 };
759 Ok(if external_pipette {
760 builder.with_external_pipette()
761 } else {
762 builder
763 })
764 }
765
766 fn clear_minimal_mode(mut self) -> Self {
767 self.minimal_mode = false;
768 self
769 }
770
771 pub fn with_shared_directory(
775 mut self,
776 tag: &str,
777 host_path: impl AsRef<Path>,
778 guest_path: &str,
779 ) -> Self {
780 assert!(
781 T::supports_file_sharing(&self.config.firmware),
782 "directory sharing requires a backend with Linux file-sharing support"
783 );
784 assert!(
785 !tag.is_empty()
786 && !tag.starts_with('-')
787 && tag
788 .bytes()
789 .all(|c| c.is_ascii_alphanumeric() || b"-_".contains(&c)),
790 "invalid filesystem share tag"
791 );
792 assert!(
793 guest_path.starts_with('/')
794 && !guest_path.contains('\0')
795 && !guest_path.split('/').any(|c| c == ".."),
796 "guest mount point must be an absolute path without parent traversal"
797 );
798 assert!(
799 !self.config.guest_shares.contains_key(tag),
800 "duplicate filesystem share tag: {tag}"
801 );
802 self.config.guest_shares.insert(
803 tag.into(),
804 GuestShare {
805 host_path: host_path.as_ref().to_path_buf(),
806 guest_path: guest_path.into(),
807 },
808 );
809 self
810 }
811
812 fn with_capture_inspect_on_failure(mut self) -> Self {
813 self.capture_inspect_on_failure = true;
814 self
815 }
816
817 pub fn is_minimal(&self) -> bool {
819 self.minimal_mode
820 }
821
822 pub fn with_watchdog_timeout(mut self, timeout: Option<Duration>) -> Self {
825 self.watchdog_timeout = timeout;
826 self
827 }
828
829 pub fn with_external_pipette(mut self) -> Self {
835 self.external_pipette = true;
836 self.agent_image = None;
837 self.pipette_binary = None;
838 self
839 }
840
841 pub fn with_prebuilt_initrd(mut self, initrd: PetriInitrd) -> Self {
848 self.resources.prebuilt_initrd = Some(initrd);
849 self
850 }
851
852 pub fn prepare_initrd(&self) -> anyhow::Result<PetriInitrd> {
863 self.prepare_custom_initrd(T::build_custom_init_script)
864 }
865
866 pub fn prepare_custom_initrd(
868 &self,
869 build_custom_init_script: impl FnOnce(&str) -> Option<String>,
870 ) -> anyhow::Result<PetriInitrd> {
871 const PIPETTE_PATH: &str = "pipette";
872 const INIT_SCRIPT_NAME: &str = "custom-init.sh";
873
874 let initrd_path = self
875 .config
876 .firmware
877 .linux_direct_initrd()
878 .context("prepare_initrd requires Linux direct boot with initrd")?;
879 let pipette_path = self
880 .pipette_binary
881 .as_ref()
882 .context("prepare_initrd requires a pipette binary")?;
883
884 let pipette_data = std::fs::read(pipette_path.get()).with_context(|| {
885 format!(
886 "failed to read pipette binary at {}",
887 pipette_path.get().display()
888 )
889 })?;
890
891 let mut files = vec![crate::initrd::InitrdFile {
892 name: PIPETTE_PATH,
893 contents: &pipette_data,
894 permissions: 0o755,
895 }];
896 let script = build_custom_init_script(PIPETTE_PATH);
897 let rdinit = if let Some(script) = &script {
898 files.push(crate::initrd::InitrdFile {
899 name: INIT_SCRIPT_NAME,
900 contents: script.as_bytes(),
901 permissions: 0o755,
902 });
903 format!("/{INIT_SCRIPT_NAME}")
904 } else {
905 format!("/{PIPETTE_PATH}")
906 };
907 PetriInitrd::prepare(initrd_path, &files, &rdinit, None)
908 }
909
910 pub fn with_serial_output(mut self) -> Self {
919 self.enable_serial = true;
920 self
921 }
922
923 pub fn without_serial_output(mut self) -> Self {
928 self.enable_serial = false;
929 self
930 }
931
932 pub fn without_screenshots(mut self) -> Self {
937 self.enable_screenshots = false;
938 self
939 }
940
941 pub fn with_virtio_vsock(mut self, enable: bool) -> Self {
954 self.use_virtio_vsock = enable;
955 #[cfg(target_os = "linux")]
956 {
957 self.vhost_vsock_guest_cid = None;
958 }
959 self
960 }
961
962 #[cfg(target_os = "linux")]
968 pub fn with_vhost_vsock(mut self, guest_cid: u32) -> Self {
969 assert!(
970 (3..u32::MAX).contains(&guest_cid),
971 "vhost-vsock guest CID must be between 3 and {}",
972 u32::MAX - 1
973 );
974 self.use_virtio_vsock = true;
975 self.vhost_vsock_guest_cid = Some(guest_cid);
976 self
977 }
978
979 pub fn with_no_vmbus(mut self) -> Self {
987 self.no_vmbus = true;
988 if self.config.firmware.os_flavor() != OsFlavor::Windows {
989 self.use_virtio_vsock = true;
990 }
991 self.config.vmbus_storage_controllers.clear();
992 self
993 }
994
995 pub fn with_no_hv(mut self) -> Self {
1001 self.no_hv = true;
1002 self.with_no_vmbus()
1003 }
1004
1005 fn add_petri_scsi_controllers(mut self) -> Self {
1006 if T::SUPPORTS_VMBUS {
1007 self = self.add_vmbus_storage_controller(
1008 &PETRI_SCSI_VTL0_CONTROLLER,
1009 Vtl::Vtl0,
1010 VmbusStorageType::Scsi,
1011 );
1012
1013 if self.is_openhcl() {
1014 self = self.add_vmbus_storage_controller(
1015 &PETRI_SCSI_VTL2_CONTROLLER,
1016 Vtl::Vtl2,
1017 VmbusStorageType::Scsi,
1018 )
1019 }
1020 }
1021 self
1022 }
1023
1024 fn add_guest_crash_disk(
1025 self,
1026 post_test_hooks: &mut Vec<PetriPostTestHook>,
1027 ) -> anyhow::Result<Self> {
1028 let supports_vmbus_crash_disk = !self.no_vmbus
1029 && matches!(
1030 self.config.firmware.os_flavor(),
1031 OsFlavor::Windows | OsFlavor::Linux
1032 );
1033
1034 let (disk, disk_hook) = supports_vmbus_crash_disk
1035 .then(|| T::create_guest_dump_disk().context("failed to create guest dump disk"))
1036 .transpose()?
1037 .flatten()
1038 .unzip();
1039
1040 if let Some(disk_hook) = disk_hook {
1041 let logger = self.resources.log_source.clone();
1042 post_test_hooks.push(PetriPostTestHook::new(
1043 "extract guest crash dumps".into(),
1044 move |test_passed| {
1045 if test_passed {
1046 return Ok(());
1047 }
1048 let mut disk = disk_hook()?;
1049 let gpt = gptman::GPT::read_from(&mut disk, SECTOR_SIZE)?;
1050 let partition = fscommon::StreamSlice::new(
1051 &mut disk,
1052 gpt[1].starting_lba * SECTOR_SIZE,
1053 gpt[1].ending_lba * SECTOR_SIZE,
1054 )?;
1055 let fs = fatfs::FileSystem::new(partition, fatfs::FsOptions::new())?;
1056 for entry in fs.root_dir().iter() {
1057 let Ok(entry) = entry else {
1058 tracing::warn!(?entry, "failed to read entry in guest crash dump disk");
1059 continue;
1060 };
1061 if !entry.is_file() {
1062 tracing::warn!(
1063 ?entry,
1064 "skipping non-file entry in guest crash dump disk"
1065 );
1066 continue;
1067 }
1068 logger.write_attachment(&entry.file_name(), entry.to_file())?;
1069 }
1070 Ok(())
1071 },
1072 ));
1073 }
1074
1075 Ok(if let Some(disk) = disk {
1076 self.add_vmbus_drive(
1077 Drive::new(Some(Disk::Temporary(disk)), false),
1078 &PETRI_SCSI_VTL0_CONTROLLER,
1079 Some(PETRI_SCSI_CRASH_LUN),
1080 )
1081 } else {
1082 self
1083 })
1084 }
1085
1086 fn add_agent_disks(self) -> anyhow::Result<Self> {
1087 self.add_agent_disk_inner(Vtl::Vtl0)?
1088 .add_agent_disk_inner(Vtl::Vtl2)
1089 }
1090
1091 fn add_agent_disk_inner(mut self, target_vtl: Vtl) -> anyhow::Result<Self> {
1092 let (agent_image, controller_id) = match target_vtl {
1093 Vtl::Vtl0 => (self.agent_image.as_ref(), PETRI_SCSI_VTL0_CONTROLLER),
1094 Vtl::Vtl1 => panic!("no VTL1 agent disk"),
1095 Vtl::Vtl2 => (
1096 self.openhcl_agent_image.as_ref(),
1097 PETRI_SCSI_VTL2_CONTROLLER,
1098 ),
1099 };
1100
1101 if target_vtl == Vtl::Vtl0
1104 && self.uses_pipette_as_init()
1105 && !agent_image.is_some_and(|i| i.has_extras())
1106 {
1107 return Ok(self);
1108 }
1109
1110 let Some(agent_disk) = agent_image
1111 .map(|i| {
1112 i.build(crate::disk_image::ImageType::Vhd)
1113 .context("failed to build agent image")
1114 })
1115 .transpose()?
1116 .flatten()
1117 else {
1118 return Ok(self);
1119 };
1120
1121 if self.no_vmbus {
1124 self.config.pcie_nvme_drives.push(PcieNvmeDrive {
1125 port_name: PETRI_PCIE_NVME_AGENT_PORT.into(),
1126 nsid: PETRI_PCIE_NVME_AGENT_NSID,
1127 drive: Drive::new(
1128 Some(Disk::Temporary(Arc::new(agent_disk.into_temp_path()))),
1129 false,
1130 ),
1131 });
1132 return Ok(self);
1133 }
1134
1135 if !self
1138 .config
1139 .vmbus_storage_controllers
1140 .contains_key(&controller_id)
1141 {
1142 self = self.add_vmbus_storage_controller(
1143 &controller_id,
1144 target_vtl,
1145 VmbusStorageType::Scsi,
1146 );
1147 }
1148
1149 Ok(self.add_vmbus_drive(
1150 Drive::new(
1151 Some(Disk::Temporary(Arc::new(agent_disk.into_temp_path()))),
1152 false,
1153 ),
1154 &controller_id,
1155 Some(PETRI_SCSI_PIPETTE_LUN),
1156 ))
1157 }
1158
1159 fn add_boot_disk(mut self) -> anyhow::Result<Self> {
1160 if self.boot_device_type.requires_vtl2() && !self.is_openhcl() {
1161 panic!("boot device type {:?} requires vtl2", self.boot_device_type);
1162 }
1163
1164 if self.no_vmbus && self.boot_device_type.requires_vmbus() {
1165 panic!(
1166 "boot device type {:?} requires vmbus, but vmbus is disabled; \
1167 use with_boot_device_type(BootDeviceType::PcieNvme) or similar",
1168 self.boot_device_type
1169 );
1170 }
1171
1172 if self.boot_device_type.requires_vpci_boot() {
1173 self.config
1174 .firmware
1175 .uefi_config_mut()
1176 .expect("vpci boot requires uefi")
1177 .enable_vpci_boot = true;
1178 }
1179
1180 let builder = if let Some(boot_drive) = self.config.firmware.boot_drive()? {
1181 match self.boot_device_type {
1182 BootDeviceType::None => unreachable!(),
1183 BootDeviceType::Ide => self.add_ide_drive(
1184 boot_drive,
1185 PETRI_IDE_BOOT_CONTROLLER_NUMBER,
1186 PETRI_IDE_BOOT_LUN,
1187 ),
1188 BootDeviceType::IdeViaScsi => self
1189 .add_vmbus_drive(
1190 boot_drive,
1191 &PETRI_SCSI_VTL2_CONTROLLER,
1192 Some(PETRI_SCSI_BOOT_LUN),
1193 )
1194 .add_vtl2_storage_controller(
1195 Vtl2StorageControllerBuilder::new(ControllerType::Ide)
1196 .with_instance_id(PETRI_IDE_BOOT_CONTROLLER)
1197 .add_lun(
1198 Vtl2LunBuilder::disk()
1199 .with_channel(PETRI_IDE_BOOT_CONTROLLER_NUMBER)
1200 .with_location(PETRI_IDE_BOOT_LUN as u32)
1201 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
1202 ControllerType::Scsi,
1203 PETRI_SCSI_VTL2_CONTROLLER,
1204 PETRI_SCSI_BOOT_LUN,
1205 )),
1206 )
1207 .build(),
1208 ),
1209 BootDeviceType::IdeViaNvme => todo!(),
1210 BootDeviceType::Scsi => self.add_vmbus_drive(
1211 boot_drive,
1212 &PETRI_SCSI_VTL0_CONTROLLER,
1213 Some(PETRI_SCSI_BOOT_LUN),
1214 ),
1215 BootDeviceType::ScsiViaScsi => self
1216 .add_vmbus_drive(
1217 boot_drive,
1218 &PETRI_SCSI_VTL2_CONTROLLER,
1219 Some(PETRI_SCSI_BOOT_LUN),
1220 )
1221 .add_vtl2_storage_controller(
1222 Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
1223 .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
1224 .add_lun(
1225 Vtl2LunBuilder::disk()
1226 .with_location(PETRI_SCSI_BOOT_LUN)
1227 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
1228 ControllerType::Scsi,
1229 PETRI_SCSI_VTL2_CONTROLLER,
1230 PETRI_SCSI_BOOT_LUN,
1231 )),
1232 )
1233 .build(),
1234 ),
1235 BootDeviceType::ScsiViaNvme => self
1236 .add_vmbus_storage_controller(
1237 &PETRI_NVME_BOOT_VTL2_CONTROLLER,
1238 Vtl::Vtl2,
1239 VmbusStorageType::Nvme,
1240 )
1241 .add_vmbus_drive(
1242 boot_drive,
1243 &PETRI_NVME_BOOT_VTL2_CONTROLLER,
1244 Some(PETRI_NVME_BOOT_NSID),
1245 )
1246 .add_vtl2_storage_controller(
1247 Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
1248 .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
1249 .add_lun(
1250 Vtl2LunBuilder::disk()
1251 .with_location(PETRI_SCSI_BOOT_LUN)
1252 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
1253 ControllerType::Nvme,
1254 PETRI_NVME_BOOT_VTL2_CONTROLLER,
1255 PETRI_NVME_BOOT_NSID,
1256 )),
1257 )
1258 .build(),
1259 ),
1260 BootDeviceType::Nvme => self
1261 .add_vmbus_storage_controller(
1262 &PETRI_NVME_BOOT_VTL0_CONTROLLER,
1263 Vtl::Vtl0,
1264 VmbusStorageType::Nvme,
1265 )
1266 .add_vmbus_drive(
1267 boot_drive,
1268 &PETRI_NVME_BOOT_VTL0_CONTROLLER,
1269 Some(PETRI_NVME_BOOT_NSID),
1270 ),
1271 BootDeviceType::NvmeViaScsi => todo!(),
1272 BootDeviceType::NvmeViaNvme => todo!(),
1273 BootDeviceType::PcieNvme => {
1274 let port_name = self
1275 .pcie_boot_port
1276 .clone()
1277 .unwrap_or_else(|| "s0rc0rp0".into());
1278 self.config.pcie_nvme_drives.push(PcieNvmeDrive {
1279 port_name,
1280 nsid: 1,
1281 drive: boot_drive,
1282 });
1283 self
1284 }
1285 BootDeviceType::PcieVirtioBlk => {
1286 self.config.pcie_virtio_blk_drives.push(PcieVirtioBlkDrive {
1287 port_name: self
1288 .pcie_boot_port
1289 .clone()
1290 .unwrap_or_else(|| "s0rc0rp0".into()),
1291 drive: boot_drive,
1292 });
1293 self
1294 }
1295 }
1296 } else {
1297 self
1298 };
1299 Ok(builder)
1300 }
1301
1302 fn has_agent_disk(&self) -> bool {
1307 if self.uses_pipette_as_init() {
1308 self.agent_image.as_ref().is_some_and(|i| i.has_extras())
1309 } else {
1310 self.agent_image.is_some()
1311 }
1312 }
1313
1314 pub fn properties(&self) -> PetriVmProperties {
1316 PetriVmProperties {
1317 is_openhcl: self.config.firmware.is_openhcl(),
1318 is_isolated: self.config.firmware.isolation().is_some(),
1319 is_pcat: self.config.firmware.is_pcat(),
1320 is_linux_direct: self.config.firmware.is_linux_direct(),
1321 using_vtl0_pipette: self.using_vtl0_pipette(),
1322 external_pipette: self.external_pipette,
1323 using_vpci: self.boot_device_type.requires_vpci_boot(),
1324 os_flavor: self.config.firmware.os_flavor(),
1325 minimal_mode: self.minimal_mode,
1326 uses_pipette_as_init: self.uses_pipette_as_init(),
1327 enable_serial: self.enable_serial,
1328 has_agent_disk: self.has_agent_disk(),
1329 use_virtio_vsock: self.use_virtio_vsock,
1330 #[cfg(target_os = "linux")]
1331 vhost_vsock_guest_cid: self.vhost_vsock_guest_cid,
1332 no_vmbus: self.no_vmbus,
1333 no_hv: self.no_hv,
1334 }
1335 }
1336
1337 fn uses_pipette_as_init(&self) -> bool {
1343 self.config.firmware.is_linux_direct()
1344 && !self.config.firmware.is_openhcl()
1345 && self.pipette_binary.is_some()
1346 }
1347
1348 pub fn using_vtl0_pipette(&self) -> bool {
1350 self.external_pipette
1351 || self.uses_pipette_as_init()
1352 || self
1353 .agent_image
1354 .as_ref()
1355 .is_some_and(|x| x.contains_pipette())
1356 }
1357
1358 pub async fn run_without_agent(self) -> anyhow::Result<PetriVm<T>> {
1362 self.run_core().await
1363 }
1364
1365 pub async fn run(self) -> anyhow::Result<(PetriVm<T>, PipetteClient)> {
1368 assert!(self.using_vtl0_pipette());
1369
1370 let mut vm = self.add_nested_artifacts_to_agent_disk().run_core().await?;
1371 let client = vm.wait_for_agent().await?;
1372
1373 Ok((vm, client))
1374 }
1375
1376 async fn run_core(mut self) -> anyhow::Result<PetriVm<T>> {
1377 self = self.add_boot_disk()?.add_agent_disks()?;
1380
1381 if self.uses_pipette_as_init() && self.resources.prebuilt_initrd.is_none() {
1385 self.resources.prebuilt_initrd = Some(self.prepare_initrd()?);
1386 }
1387
1388 tracing::debug!(builder = ?self);
1389
1390 let arch = self.config.arch;
1391 let expect_reset = self.expect_reset();
1392 let properties = self.properties();
1393 let guest_shares = self.config.guest_shares.clone();
1394 let name = self.config.name.clone();
1395
1396 let (mut runtime, config) = self
1397 .backend
1398 .run(
1399 self.config,
1400 self.modify_vmm_config,
1401 &self.resources,
1402 &properties,
1403 )
1404 .await?;
1405 let openhcl_diag_handler = runtime.openhcl_diag();
1406 let watchdog_tasks = Self::start_watchdog_tasks(
1407 &self.resources,
1408 &mut runtime,
1409 self.enable_screenshots,
1410 self.watchdog_timeout,
1411 )?;
1412
1413 let mut vm = PetriVm {
1414 runtime: PetriVmRuntimeGuard {
1415 runtime: Some(runtime),
1416 driver: self.resources.driver.clone(),
1417 log_source: self.resources.log_source.clone(),
1418 capture_inspect_on_drop: self.capture_inspect_on_failure,
1419 },
1420 resources: self.resources,
1421 watchdog_tasks,
1422 openhcl_diag_handler,
1423
1424 arch,
1425 guest_quirks: self.guest_quirks,
1426 vmm_quirks: self.vmm_quirks,
1427 expected_boot_event: self.expected_boot_event,
1428
1429 config,
1430 properties,
1431
1432 nested_test: self.nested_test,
1433 guest_shares,
1434 name,
1435 };
1436
1437 if expect_reset {
1438 vm.wait_for_reset_core().await?;
1439 }
1440
1441 vm.wait_for_expected_boot_event().await?;
1442
1443 Ok(vm)
1444 }
1445
1446 fn expect_reset(&self) -> bool {
1447 self.override_expect_reset
1448 || matches!(
1449 (
1450 self.guest_quirks.initial_reboot,
1451 self.expected_boot_event,
1452 &self.config.firmware,
1453 &self.config.tpm,
1454 ),
1455 (
1456 Some(InitialRebootCondition::Always),
1457 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
1458 _,
1459 _,
1460 ) | (
1461 Some(InitialRebootCondition::WithTpm),
1462 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
1463 _,
1464 Some(_),
1465 )
1466 )
1467 }
1468
1469 fn start_watchdog_tasks(
1470 resources: &PetriVmResources,
1471 runtime: &mut T::VmRuntime,
1472 enable_screenshots: bool,
1473 watchdog_timeout: Option<Duration>,
1474 ) -> anyhow::Result<Vec<Task<()>>> {
1475 let mut tasks = Vec::new();
1476
1477 if let Some(timeout) = watchdog_timeout {
1478 let driver = resources.driver.clone();
1481 tasks.push(resources.driver.spawn("timer-watchdog", async move {
1482 PolledTimer::new(&driver).sleep(timeout).await;
1483 tracing::error!(?timeout, "Test timeout reached, aborting.");
1484 panic!("Test timed out");
1485 }));
1486 }
1487
1488 if enable_screenshots {
1489 if let Some(mut framebuffer_access) = runtime.take_framebuffer_access() {
1490 let mut timer = PolledTimer::new(&resources.driver);
1491 let log_source = resources.log_source.clone();
1492
1493 tasks.push(
1494 resources
1495 .driver
1496 .spawn("petri-watchdog-screenshot", async move {
1497 let mut image = Vec::new();
1498 let mut last_image = Vec::new();
1499 loop {
1500 timer.sleep(Duration::from_secs(2)).await;
1501 tracing::trace!("Taking screenshot.");
1502
1503 let VmScreenshotMeta {
1504 color,
1505 width,
1506 height,
1507 } = match framebuffer_access.screenshot(&mut image).await {
1508 Ok(Some(meta)) => meta,
1509 Ok(None) => {
1510 tracing::debug!("VM off, skipping screenshot.");
1511 continue;
1512 }
1513 Err(e) => {
1514 tracing::error!(?e, "Failed to take screenshot");
1515 continue;
1516 }
1517 };
1518
1519 if image == last_image {
1520 tracing::debug!(
1521 "No change in framebuffer, skipping screenshot."
1522 );
1523 continue;
1524 }
1525
1526 let r = log_source.create_attachment("screenshot.png").and_then(
1527 |mut f| {
1528 image::write_buffer_with_format(
1529 &mut f,
1530 &image,
1531 width.into(),
1532 height.into(),
1533 color,
1534 image::ImageFormat::Png,
1535 )
1536 .map_err(Into::into)
1537 },
1538 );
1539
1540 if let Err(e) = r {
1541 tracing::error!(?e, "Failed to save screenshot");
1542 } else {
1543 tracing::info!("Screenshot saved.");
1544 }
1545
1546 std::mem::swap(&mut image, &mut last_image);
1547 }
1548 }),
1549 );
1550 }
1551 }
1552
1553 Ok(tasks)
1554 }
1555
1556 pub fn with_expect_boot_failure(mut self) -> Self {
1559 self.expected_boot_event = Some(FirmwareEvent::BootFailed);
1560 self
1561 }
1562
1563 pub fn with_expect_no_boot_event(mut self) -> Self {
1566 self.expected_boot_event = None;
1567 self
1568 }
1569
1570 pub fn with_expect_reset(mut self) -> Self {
1574 self.override_expect_reset = true;
1575 self
1576 }
1577
1578 pub fn with_secure_boot(mut self) -> Self {
1580 self.config
1581 .firmware
1582 .uefi_config_mut()
1583 .expect("Secure boot is only supported for UEFI firmware.")
1584 .secure_boot_enabled = true;
1585
1586 match self.os_flavor() {
1587 OsFlavor::Windows => self.with_windows_secure_boot_template(),
1588 OsFlavor::Linux => self.with_uefi_ca_secure_boot_template(),
1589 _ => panic!(
1590 "Secure boot unsupported for OS flavor {:?}",
1591 self.os_flavor()
1592 ),
1593 }
1594 }
1595
1596 pub fn with_windows_secure_boot_template(mut self) -> Self {
1598 self.config
1599 .firmware
1600 .uefi_config_mut()
1601 .expect("Secure boot is only supported for UEFI firmware.")
1602 .secure_boot_template = Some(SecureBootTemplate::MicrosoftWindows);
1603 self
1604 }
1605
1606 pub fn with_uefi_ca_secure_boot_template(mut self) -> Self {
1608 self.config
1609 .firmware
1610 .uefi_config_mut()
1611 .expect("Secure boot is only supported for UEFI firmware.")
1612 .secure_boot_template = Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority);
1613 self
1614 }
1615
1616 pub fn with_custom_uefi_json(mut self, json: impl Into<Vec<u8>>) -> Self {
1618 self.config
1619 .firmware
1620 .uefi_config_mut()
1621 .expect("Custom UEFI variables are only supported for UEFI firmware.")
1622 .custom_uefi_json = Some(json.into());
1623 self
1624 }
1625
1626 pub fn with_processor_topology(mut self, topology: ProcessorTopology) -> Self {
1628 self.config.proc_topology = topology;
1629 self
1630 }
1631
1632 pub fn with_memory(mut self, memory: MemoryConfig) -> Self {
1634 self.config.memory = memory;
1635 self
1636 }
1637
1638 pub fn with_vtl2_base_address_type(mut self, address_type: Vtl2BaseAddressType) -> Self {
1643 self.config
1644 .firmware
1645 .openhcl_config_mut()
1646 .expect("OpenHCL firmware is required to set custom VTL2 address type.")
1647 .vtl2_base_address_type = Some(address_type);
1648 self
1649 }
1650
1651 pub fn with_custom_openhcl(mut self, artifact: ResolvedArtifact<impl IsOpenhclIgvm>) -> Self {
1653 match &mut self.config.firmware {
1654 Firmware::OpenhclLinuxDirect { igvm_firmware, .. }
1655 | Firmware::OpenhclPcat { igvm_firmware, .. }
1656 | Firmware::OpenhclUefi { igvm_firmware, .. } => {
1657 *igvm_firmware = IgvmFirmwareSource::File(artifact.erase());
1658 }
1659 Firmware::LinuxDirect { .. }
1660 | Firmware::IgvmLinuxDirect { .. }
1661 | Firmware::Uefi { .. }
1662 | Firmware::Pcat { .. } => {
1663 panic!("Custom OpenHCL is only supported for OpenHCL firmware.")
1664 }
1665 }
1666 self
1667 }
1668
1669 pub fn with_openhcl_from_vmgs(mut self) -> Self {
1671 match &mut self.config.firmware {
1672 Firmware::OpenhclLinuxDirect { igvm_firmware, .. }
1673 | Firmware::OpenhclPcat { igvm_firmware, .. }
1674 | Firmware::OpenhclUefi { igvm_firmware, .. } => {
1675 *igvm_firmware = IgvmFirmwareSource::VmgsOrInBox;
1676 }
1677 Firmware::LinuxDirect { .. }
1678 | Firmware::IgvmLinuxDirect { .. }
1679 | Firmware::Uefi { .. }
1680 | Firmware::Pcat { .. } => {
1681 panic!("Loading OpenHCL from VMGS is only supported for OpenHCL firmware.")
1682 }
1683 }
1684 self
1685 }
1686
1687 pub fn with_openhcl_command_line(mut self, additional_command_line: &str) -> Self {
1689 append_cmdline(
1690 &mut self
1691 .config
1692 .firmware
1693 .openhcl_config_mut()
1694 .expect("OpenHCL command line is only supported for OpenHCL firmware.")
1695 .custom_command_line,
1696 additional_command_line,
1697 );
1698 self
1699 }
1700
1701 pub fn with_mana_keepalive(mut self, enable: bool) -> Self {
1703 self.config
1704 .firmware
1705 .openhcl_config_mut()
1706 .expect("MANA keepalive is only supported for OpenHCL firmware.")
1707 .enable_mana_keepalive = enable;
1708 self
1709 }
1710
1711 pub fn with_confidential_filtering(self) -> Self {
1713 if !self.config.firmware.is_openhcl() {
1714 panic!("Confidential filtering is only supported for OpenHCL");
1715 }
1716 self.with_openhcl_command_line(&format!(
1717 "{}=1 {}=0",
1718 underhill_confidentiality::OPENHCL_CONFIDENTIAL_ENV_VAR_NAME,
1719 underhill_confidentiality::OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME
1720 ))
1721 }
1722
1723 pub fn with_openhcl_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1725 self.config
1726 .firmware
1727 .openhcl_config_mut()
1728 .expect("OpenHCL firmware is required to set custom OpenHCL log levels.")
1729 .log_levels = levels;
1730 self
1731 }
1732
1733 pub fn with_host_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1737 if let OpenvmmLogConfig::Custom(ref custom_levels) = levels {
1738 for key in custom_levels.keys() {
1739 if !["OPENVMM_LOG", "OPENVMM_SHOW_SPANS"].contains(&key.as_str()) {
1740 panic!("Unsupported OpenVMM log level key: {}", key);
1741 }
1742 }
1743 }
1744
1745 self.config.host_log_levels = Some(levels.clone());
1746 self
1747 }
1748
1749 pub fn with_agent_file(mut self, name: &str, file_path: impl AsRef<Path>) -> Self {
1751 self.agent_image
1752 .as_mut()
1753 .expect("no guest pipette")
1754 .add_file(name, file_path);
1755 self
1756 }
1757
1758 pub fn with_openhcl_agent_file(mut self, name: &str, file_path: impl AsRef<Path>) -> Self {
1760 self.openhcl_agent_image
1761 .as_mut()
1762 .expect("no openhcl pipette")
1763 .add_file(name, file_path);
1764 self
1765 }
1766
1767 pub fn with_uefi_frontpage(mut self, enable: bool) -> Self {
1769 self.config
1770 .firmware
1771 .uefi_config_mut()
1772 .expect("UEFI frontpage is only supported for UEFI firmware.")
1773 .disable_frontpage = !enable;
1774 self
1775 }
1776
1777 pub fn with_efi_diagnostics_log_level(mut self, level: EfiDiagnosticsLogLevel) -> Self {
1783 self.config
1784 .firmware
1785 .uefi_config_mut()
1786 .expect("EFI diagnostics log level is only supported for UEFI firmware.")
1787 .efi_diagnostics_log_level = level;
1788 self
1789 }
1790
1791 pub fn with_efi_diagnostics_rate_limit(mut self, limit: u32) -> Self {
1797 self.config
1798 .firmware
1799 .uefi_config_mut()
1800 .expect("EFI diagnostics rate limit is only supported for UEFI firmware.")
1801 .efi_diagnostics_rate_limit = Some(limit);
1802 self
1803 }
1804
1805 pub fn with_default_boot_always_attempt(mut self, enable: bool) -> Self {
1807 self.config
1808 .firmware
1809 .uefi_config_mut()
1810 .expect("Default boot always attempt is only supported for UEFI firmware.")
1811 .default_boot_always_attempt = enable;
1812 self
1813 }
1814
1815 pub fn with_uefi_force_dma_bounce(mut self, enable: bool) -> Self {
1817 self.config
1818 .firmware
1819 .uefi_config_mut()
1820 .expect("force DMA bounce is only supported for UEFI firmware.")
1821 .force_dma_bounce = enable;
1822 self
1823 }
1824
1825 pub fn with_vmbus_redirect(mut self, enable: bool) -> Self {
1827 self.config
1828 .firmware
1829 .openhcl_config_mut()
1830 .expect("VMBus redirection is only supported for OpenHCL firmware.")
1831 .vmbus_redirect = enable;
1832 self
1833 }
1834
1835 pub fn with_hibernation_enabled(mut self, enable: bool) -> Self {
1841 self.config.hibernation_enabled = enable;
1842 self
1843 }
1844
1845 pub fn with_ipmi(mut self, enable: bool) -> Self {
1847 self.config.ipmi_enabled = enable;
1848 self
1849 }
1850
1851 pub fn with_guest_state_lifetime(
1853 mut self,
1854 guest_state_lifetime: PetriGuestStateLifetime,
1855 ) -> Self {
1856 let disk = match self.config.vmgs {
1857 PetriVmgsResource::Disk(disk)
1858 | PetriVmgsResource::ReprovisionOnFailure(disk)
1859 | PetriVmgsResource::Reprovision(disk) => disk,
1860 PetriVmgsResource::Ephemeral => PetriVmgsDisk::default(),
1861 };
1862 self.config.vmgs = match guest_state_lifetime {
1863 PetriGuestStateLifetime::Disk => PetriVmgsResource::Disk(disk),
1864 PetriGuestStateLifetime::ReprovisionOnFailure => {
1865 PetriVmgsResource::ReprovisionOnFailure(disk)
1866 }
1867 PetriGuestStateLifetime::Reprovision => PetriVmgsResource::Reprovision(disk),
1868 PetriGuestStateLifetime::Ephemeral => {
1869 if !matches!(disk.disk, Disk::Memory(_)) {
1870 panic!("attempted to use ephemeral guest state after specifying backing vmgs")
1871 }
1872 PetriVmgsResource::Ephemeral
1873 }
1874 };
1875 self
1876 }
1877
1878 pub fn with_guest_state_encryption(mut self, policy: GuestStateEncryptionPolicy) -> Self {
1880 match &mut self.config.vmgs {
1881 PetriVmgsResource::Disk(vmgs)
1882 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1883 | PetriVmgsResource::Reprovision(vmgs) => {
1884 vmgs.encryption_policy = policy;
1885 }
1886 PetriVmgsResource::Ephemeral => {
1887 panic!("attempted to encrypt ephemeral guest state")
1888 }
1889 }
1890 self
1891 }
1892
1893 pub fn with_initial_vmgs(self, disk: ResolvedArtifact<impl IsTestVmgs>) -> Self {
1895 self.with_backing_vmgs(Disk::Differencing(DiskPath::Local(disk.into())))
1896 }
1897
1898 pub fn with_persistent_vmgs(self, disk: impl AsRef<Path>) -> Self {
1900 self.with_backing_vmgs(Disk::Persistent(disk.as_ref().to_path_buf()))
1901 }
1902
1903 fn with_backing_vmgs(mut self, disk: Disk) -> Self {
1904 match &mut self.config.vmgs {
1905 PetriVmgsResource::Disk(vmgs)
1906 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1907 | PetriVmgsResource::Reprovision(vmgs) => {
1908 if !matches!(vmgs.disk, Disk::Memory(_)) {
1909 panic!("already specified a backing vmgs file");
1910 }
1911 vmgs.disk = disk;
1912 }
1913 PetriVmgsResource::Ephemeral => {
1914 panic!("attempted to specify a backing vmgs with ephemeral guest state")
1915 }
1916 }
1917 self
1918 }
1919
1920 pub fn with_boot_device_type(mut self, boot: BootDeviceType) -> Self {
1924 self.boot_device_type = boot;
1925 self
1926 }
1927
1928 pub fn with_pcie_boot_port(mut self, port_name: &str) -> Self {
1934 self.pcie_boot_port = Some(port_name.to_string());
1935 self
1936 }
1937
1938 pub fn with_tpm(mut self, enable: bool) -> Self {
1940 if enable {
1941 self.config.tpm.get_or_insert_default();
1942 } else {
1943 self.config.tpm = None;
1944 }
1945 self
1946 }
1947
1948 pub fn with_tpm_state_persistence(mut self, tpm_state_persistence: bool) -> Self {
1950 self.config
1951 .tpm
1952 .as_mut()
1953 .expect("TPM persistence requires a TPM")
1954 .no_persistent_secrets = !tpm_state_persistence;
1955 self
1956 }
1957
1958 pub fn with_hardware_sealing_policy(mut self, policy: PetriHardwareSealingPolicy) -> Self {
1960 self.config
1961 .tpm
1962 .as_mut()
1963 .expect("hardware sealing policy requires a TPM")
1964 .hardware_sealing_policy = policy;
1965 self
1966 }
1967
1968 pub fn with_tpm_version(mut self, version: PetriTpmVersion) -> Self {
1970 self.config
1971 .tpm
1972 .as_mut()
1973 .expect("TPM version requires a TPM")
1974 .version = version;
1975 self
1976 }
1977
1978 pub fn with_custom_vtl2_settings(
1982 mut self,
1983 f: impl FnOnce(&mut Vtl2Settings) + 'static + Send + Sync,
1984 ) -> Self {
1985 f(self
1986 .config
1987 .firmware
1988 .vtl2_settings()
1989 .expect("Custom VTL 2 settings are only supported with OpenHCL"));
1990 self
1991 }
1992
1993 pub fn add_vtl2_storage_controller(self, controller: StorageController) -> Self {
1995 self.with_custom_vtl2_settings(move |v| {
1996 v.dynamic
1997 .as_mut()
1998 .unwrap()
1999 .storage_controllers
2000 .push(controller)
2001 })
2002 }
2003
2004 pub fn add_vmbus_storage_controller(
2006 mut self,
2007 id: &Guid,
2008 target_vtl: Vtl,
2009 controller_type: VmbusStorageType,
2010 ) -> Self {
2011 if self
2012 .config
2013 .vmbus_storage_controllers
2014 .insert(
2015 *id,
2016 VmbusStorageController::new(target_vtl, controller_type),
2017 )
2018 .is_some()
2019 {
2020 panic!("storage controller {id} already existed");
2021 }
2022 self
2023 }
2024
2025 pub fn add_vmbus_drive(
2027 mut self,
2028 drive: Drive,
2029 controller_id: &Guid,
2030 controller_location: Option<u32>,
2031 ) -> Self {
2032 let controller = self
2033 .config
2034 .vmbus_storage_controllers
2035 .get_mut(controller_id)
2036 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
2037
2038 _ = controller.set_drive(controller_location, drive, false);
2039
2040 self
2041 }
2042
2043 pub fn add_ide_drive(
2045 mut self,
2046 drive: Drive,
2047 controller_number: u32,
2048 controller_location: u8,
2049 ) -> Self {
2050 self.config
2051 .firmware
2052 .ide_controllers_mut()
2053 .expect("Host IDE requires PCAT with no HCL")[controller_number as usize]
2054 [controller_location as usize] = Some(drive);
2055
2056 self
2057 }
2058
2059 pub fn add_physical_nvme_device(mut self, vsid: Guid, device: PhysicalNvmeDevice) -> Self {
2061 if self
2062 .config
2063 .physical_nvme_devices
2064 .insert(vsid, device)
2065 .is_some()
2066 {
2067 panic!("physical NVMe device {vsid} already existed");
2068 }
2069 self
2070 }
2071
2072 pub fn os_flavor(&self) -> OsFlavor {
2074 self.config.firmware.os_flavor()
2075 }
2076
2077 pub fn is_openhcl(&self) -> bool {
2079 self.config.firmware.is_openhcl()
2080 }
2081
2082 pub fn isolation(&self) -> Option<IsolationType> {
2084 self.config.firmware.isolation()
2085 }
2086
2087 pub fn arch(&self) -> MachineArch {
2089 self.config.arch
2090 }
2091
2092 pub fn log_source(&self) -> &PetriLogSource {
2094 &self.resources.log_source
2095 }
2096
2097 pub fn default_servicing_flags(&self) -> OpenHclServicingFlags {
2099 T::default_servicing_flags()
2100 }
2101
2102 pub fn modify_backend(
2105 mut self,
2106 f: impl FnOnce(T::VmmConfig) -> T::VmmConfig + 'static + Send,
2107 ) -> Self {
2108 let previous = self.modify_vmm_config.take();
2109 self.modify_vmm_config = Some(ModifyFn(Box::new(move |config| {
2110 let config = match previous {
2111 Some(previous) => previous.0(config),
2112 None => config,
2113 };
2114 f(config)
2115 })));
2116 self
2117 }
2118
2119 fn add_nested_artifacts_to_agent_disk(mut self) -> Self {
2120 let Some(nested_test) = self.nested_test.as_ref() else {
2121 return self;
2122 };
2123
2124 if T::supports_file_sharing(&self.config.firmware) {
2127 self.config.guest_shares.insert(
2128 NESTED_CONTENT_TAG.into(),
2129 GuestShare {
2130 host_path: PathBuf::from(
2131 std::env::var(VMM_TESTS_CONTENT_DIR).expect("test content dir not set"),
2132 ),
2133 guest_path: self.agent_disk_path(&[NESTED_TEST_CONTENT_DIR.into()]),
2134 },
2135 );
2136 self.config.guest_shares.insert(
2137 NESTED_IMAGES_TAG.into(),
2138 GuestShare {
2139 host_path: PathBuf::from(
2140 std::env::var(VMM_TEST_IMAGES).expect("test images dir not set"),
2141 ),
2142 guest_path: self.agent_disk_path(&[NESTED_IMAGES_DIR.into()]),
2143 },
2144 );
2145
2146 return self;
2147 }
2148
2149 let mut extras = Vec::new();
2150
2151 let path_builder = FatFsPathBuilder(self.os_flavor());
2152
2153 extras.push((NESTED_TEST_CONTENT_DIR.to_string(), ImageEntry::Dir));
2154 extras.push((
2155 path_builder
2156 .build_guest_path(&[NESTED_TEST_CONTENT_DIR.into(), NESTED_RESULTS_DIR.into()]),
2157 ImageEntry::Dir,
2158 ));
2159 extras.push((
2160 path_builder.build_guest_path(&[NESTED_IMAGES_DIR.into()]),
2161 ImageEntry::Dir,
2162 ));
2163 extras.push((
2164 path_builder.build_guest_path(&[
2165 NESTED_TEST_CONTENT_DIR.into(),
2166 path_builder.binary_with_extension(NESTED_TEST_BINARY),
2167 ]),
2168 ImageEntry::File(nested_test.test_binary.to_path_buf()),
2169 ));
2170
2171 for (handle, src) in &nested_test.artifacts {
2172 let relative_path = path_builder.convert_guest_path(&handle.relative_path());
2173 let dest = if handle.is_image() {
2174 path_builder.build_guest_path(&[NESTED_IMAGES_DIR.into(), relative_path])
2175 } else {
2176 path_builder.build_guest_path(&[NESTED_TEST_CONTENT_DIR.into(), relative_path])
2177 };
2178
2179 for parent in path_builder.parents(&dest) {
2180 if !extras.iter().any(|(p, _)| p == parent) {
2181 extras.push((parent.to_string(), ImageEntry::Dir))
2182 }
2183 }
2184
2185 extras.push((dest, ImageEntry::File(src.clone())));
2186 }
2187
2188 let image = self.agent_image.as_mut().expect("no guest pipette");
2189 image.add_extras(extras);
2190 image.allocate_free_space(150 * 1024 * 1024);
2192
2193 self
2194 }
2195
2196 pub fn with_nested_virt(mut self) -> Self {
2198 self.config.nested_virt_enabled = true;
2199 self
2200 }
2201
2202 pub fn with_custom_linux_kernel(
2204 mut self,
2205 custom_kernel: ResolvedArtifact<impl IsLoadable>,
2206 ) -> Self {
2207 let Firmware::LinuxDirect { kernel, .. } = &mut self.config.firmware else {
2208 panic!("VM is not linux direct");
2209 };
2210 *kernel = custom_kernel.erase();
2211 self
2212 }
2213}
2214
2215impl<T: PetriVmmBackend> PetriVm<T> {
2216 pub async fn teardown(mut self) -> anyhow::Result<()> {
2218 tracing::info!("Tearing down VM...");
2219 self.runtime.take_for_teardown().teardown().await?;
2220 Ok(())
2221 }
2222
2223 pub async fn wait_for_halt(&mut self) -> anyhow::Result<PetriHaltReasonDetail> {
2225 tracing::info!("Waiting for VM to halt...");
2226 let halt_reason = self.runtime.wait_for_halt(false).await?;
2227 tracing::info!("VM halted: {halt_reason:?}. Cancelling watchdogs...");
2228 futures::future::join_all(self.watchdog_tasks.drain(..).map(|t| t.cancel())).await;
2229 Ok(halt_reason)
2230 }
2231
2232 pub async fn wait_for_clean_shutdown(&mut self) -> anyhow::Result<()> {
2234 let halt_reason = self.wait_for_halt().await?;
2235 if halt_reason.reason != PetriHaltReason::PowerOff {
2236 anyhow::bail!("Expected PowerOff, got {halt_reason:?}");
2237 }
2238 tracing::info!("VM was cleanly powered off and torn down.");
2239 Ok(())
2240 }
2241
2242 pub async fn wait_for_teardown(mut self) -> anyhow::Result<PetriHaltReasonDetail> {
2245 let halt_reason = self.wait_for_halt().await?;
2246 self.teardown().await?;
2247 Ok(halt_reason)
2248 }
2249
2250 pub async fn wait_for_clean_teardown(mut self) -> anyhow::Result<()> {
2252 self.wait_for_clean_shutdown().await?;
2253 self.teardown().await
2254 }
2255
2256 pub async fn wait_for_reset_no_agent(&mut self) -> anyhow::Result<()> {
2258 self.wait_for_reset_core().await?;
2259 self.wait_for_expected_boot_event().await?;
2260 Ok(())
2261 }
2262
2263 pub async fn wait_for_reset(&mut self) -> anyhow::Result<PipetteClient> {
2265 self.wait_for_reset_no_agent().await?;
2266 self.wait_for_agent().await
2267 }
2268
2269 async fn wait_for_reset_core(&mut self) -> anyhow::Result<()> {
2270 tracing::info!("Waiting for VM to reset...");
2271 let halt_reason = self.runtime.wait_for_halt(true).await?;
2272 if halt_reason.reason != PetriHaltReason::Reset {
2273 anyhow::bail!("Expected reset, got {halt_reason:?}");
2274 }
2275 tracing::info!("VM reset.");
2276 Ok(())
2277 }
2278
2279 pub async fn inspect_openhcl(
2290 &self,
2291 path: impl Into<String>,
2292 depth: Option<usize>,
2293 timeout: Option<Duration>,
2294 ) -> anyhow::Result<inspect::Node> {
2295 self.openhcl_diag()?
2296 .inspect(path.into().as_str(), depth, timeout)
2297 .await
2298 }
2299
2300 pub async fn inspect_update_openhcl(
2310 &self,
2311 path: impl Into<String>,
2312 value: impl Into<String>,
2313 ) -> anyhow::Result<inspect::Value> {
2314 self.openhcl_diag()?
2315 .inspect_update(path.into(), value.into())
2316 .await
2317 }
2318
2319 pub async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()> {
2321 self.inspect_openhcl("", None, None).await.map(|_| ())
2322 }
2323
2324 pub async fn dump_openhcl_saved_state(&self) -> anyhow::Result<Vec<u8>> {
2328 self.openhcl_diag()?.dump_saved_state().await
2329 }
2330
2331 pub async fn inspect_vmm(&self, path: &str) -> anyhow::Result<inspect::Node> {
2342 use anyhow::Context;
2343
2344 let inspector = self
2345 .runtime
2346 .inspector()
2347 .context("this VMM backend does not support inspect")?;
2348 inspector.inspect(path).await
2349 }
2350
2351 pub async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()> {
2357 self.openhcl_diag()?.wait_for_vtl2().await
2358 }
2359
2360 pub async fn kmsg(&self) -> anyhow::Result<diag_client::kmsg_stream::KmsgStream> {
2362 self.openhcl_diag()?.kmsg().await
2363 }
2364
2365 pub async fn openhcl_core_dump(&self, name: &str, path: &Path) -> anyhow::Result<()> {
2368 self.openhcl_diag()?.core_dump(name, path).await
2369 }
2370
2371 pub async fn openhcl_crash(&self, name: &str) -> anyhow::Result<()> {
2373 self.openhcl_diag()?.crash(name).await
2374 }
2375
2376 async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient> {
2379 self.runtime.wait_for_enlightened_shutdown_ready().await?;
2389 let client = self.runtime.wait_for_agent(false).await?;
2390 self.mount_guest_shares(&client).await?;
2391 Ok(client)
2392 }
2393
2394 async fn mount_guest_shares(&self, client: &PipetteClient) -> anyhow::Result<()> {
2395 if !self.guest_shares.is_empty() {
2396 anyhow::ensure!(
2397 self.properties.os_flavor == OsFlavor::Linux,
2398 "directory sharing requires a Linux guest"
2399 );
2400 let mut shares = self.guest_shares.iter().collect::<Vec<_>>();
2401 shares.sort_by_key(|(tag, share)| {
2403 (
2404 share
2405 .guest_path
2406 .split('/')
2407 .filter(|part| !part.is_empty() && *part != ".")
2408 .count(),
2409 *tag,
2410 )
2411 });
2412 for (tag, share) in shares {
2413 let shell = client.unix_shell();
2414 shell
2415 .cmd("mkdir")
2416 .args(["-p", &share.guest_path])
2417 .run()
2418 .await?;
2419 shell
2420 .cmd("mount")
2421 .args([
2422 "-t",
2423 "9p",
2424 "-o",
2425 "trans=virtio,version=9p2000.L",
2426 tag,
2427 &share.guest_path,
2428 ])
2429 .run()
2430 .await
2431 .with_context(|| format!("mounting guest share {tag}"))?;
2432 }
2433 }
2434 Ok(())
2435 }
2436
2437 pub async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient> {
2441 self.launch_vtl2_pipette().await?;
2443 self.runtime.wait_for_agent(true).await
2444 }
2445
2446 async fn wait_for_expected_boot_event(&mut self) -> anyhow::Result<()> {
2453 if let Some(expected_event) = self.expected_boot_event {
2454 let event = self.wait_for_boot_event().await?;
2455
2456 anyhow::ensure!(
2457 event == expected_event,
2458 "Did not receive expected boot event"
2459 );
2460 } else {
2461 tracing::warn!("Boot event not emitted for configured firmware or manually ignored.");
2462 }
2463
2464 Ok(())
2465 }
2466
2467 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
2470 tracing::info!("Waiting for boot event...");
2471 let boot_event = loop {
2472 if let Some(event) = self
2473 .runtime
2474 .wait_for_boot_event(self.vmm_quirks.flaky_boot)
2475 .await?
2476 {
2477 break event;
2478 }
2479
2480 tracing::error!("Did not get boot event in required time, resetting...");
2481 if let Some(inspector) = self.runtime.inspector() {
2482 collect_inspect(
2483 "vmm",
2484 Box::pin(async move { inspector.inspect("").await }),
2485 &self.resources.log_source,
2486 "timeout",
2487 )
2488 .await;
2489 }
2490
2491 self.runtime.reset().await?;
2492 };
2493 tracing::info!("Got boot event: {boot_event:?}");
2494 Ok(boot_event)
2495 }
2496
2497 pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
2500 tracing::info!("Waiting for enlightened shutdown to be ready");
2501 self.runtime.wait_for_enlightened_shutdown_ready().await?;
2502
2503 let mut wait_time = Duration::from_secs(10);
2509
2510 if let Some(duration) = self.guest_quirks.hyperv_shutdown_ic_sleep {
2512 wait_time += duration;
2513 }
2514
2515 tracing::info!(
2516 "Shutdown IC reported ready, waiting for an extra {}s",
2517 wait_time.as_secs()
2518 );
2519 PolledTimer::new(&self.resources.driver)
2520 .sleep(wait_time)
2521 .await;
2522
2523 tracing::info!("Sending enlightened shutdown command");
2524 self.runtime.send_enlightened_shutdown(kind).await
2525 }
2526
2527 pub async fn restart_openhcl(
2530 &mut self,
2531 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
2532 flags: OpenHclServicingFlags,
2533 ) -> anyhow::Result<()> {
2534 self.runtime
2535 .restart_openhcl(&new_openhcl.erase(), flags)
2536 .await
2537 }
2538
2539 pub async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()> {
2542 self.runtime.update_command_line(command_line).await
2543 }
2544
2545 pub async fn add_pcie_device(
2547 &mut self,
2548 port_name: String,
2549 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
2550 ) -> anyhow::Result<()> {
2551 self.runtime.add_pcie_device(port_name, resource).await
2552 }
2553
2554 pub async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
2556 self.runtime.remove_pcie_device(port_name).await
2557 }
2558
2559 pub async fn save_openhcl(
2562 &mut self,
2563 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
2564 flags: OpenHclServicingFlags,
2565 ) -> anyhow::Result<()> {
2566 self.runtime.save_openhcl(&new_openhcl.erase(), flags).await
2567 }
2568
2569 pub async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
2572 self.runtime.restore_openhcl().await
2573 }
2574
2575 pub fn arch(&self) -> MachineArch {
2577 self.arch
2578 }
2579
2580 pub fn backend(&mut self) -> &mut T::VmRuntime {
2582 &mut self.runtime
2583 }
2584
2585 async fn launch_vtl2_pipette(&self) -> anyhow::Result<()> {
2586 tracing::debug!("Launching VTL 2 pipette...");
2587
2588 let res = self
2590 .openhcl_diag()?
2591 .run_vtl2_command("sh", &["-c", "mkdir /cidata && mount LABEL=cidata /cidata"])
2592 .await?;
2593
2594 if !res.exit_status.success() {
2595 anyhow::bail!("Failed to mount VTL 2 pipette drive: {:?}", res);
2596 }
2597
2598 let res = self
2599 .openhcl_diag()?
2600 .run_detached_vtl2_command("sh", &["-c", "/cidata/pipette 2>&1 | logger &"])
2601 .await?;
2602
2603 if !res.success() {
2604 anyhow::bail!("Failed to spawn VTL 2 pipette: {:?}", res);
2605 }
2606
2607 Ok(())
2608 }
2609
2610 fn openhcl_diag(&self) -> anyhow::Result<&OpenHclDiagHandler> {
2611 if let Some(ohd) = self.openhcl_diag_handler.as_ref() {
2612 Ok(ohd)
2613 } else {
2614 anyhow::bail!("VM is not configured with OpenHCL")
2615 }
2616 }
2617
2618 pub async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2620 self.runtime.get_guest_state_file().await
2621 }
2622
2623 pub async fn modify_vtl2_settings(
2625 &mut self,
2626 f: impl FnOnce(&mut Vtl2Settings),
2627 ) -> anyhow::Result<()> {
2628 if self.openhcl_diag_handler.is_none() {
2629 panic!("Custom VTL 2 settings are only supported with OpenHCL");
2630 }
2631 f(self
2632 .config
2633 .vtl2_settings
2634 .get_or_insert_with(default_vtl2_settings));
2635 self.runtime
2636 .set_vtl2_settings(self.config.vtl2_settings.as_ref().unwrap())
2637 .await
2638 }
2639
2640 pub fn get_vmbus_storage_controllers(&self) -> &HashMap<Guid, VmbusStorageController> {
2642 &self.config.vmbus_storage_controllers
2643 }
2644
2645 pub async fn set_vmbus_drive(
2647 &mut self,
2648 drive: Drive,
2649 controller_id: &Guid,
2650 controller_location: Option<u32>,
2651 ) -> anyhow::Result<()> {
2652 let controller = self
2653 .config
2654 .vmbus_storage_controllers
2655 .get_mut(controller_id)
2656 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
2657
2658 let controller_location = controller.set_drive(controller_location, drive, true);
2659 let disk = controller.drives.get(&controller_location).unwrap();
2660
2661 self.runtime
2662 .set_vmbus_drive(disk, controller_id, controller_location)
2663 .await?;
2664
2665 Ok(())
2666 }
2667
2668 pub async fn run_nested_test(
2670 &self,
2671 client: &PipetteClient,
2672 env: BTreeMap<String, String>,
2673 ) -> anyhow::Result<()> {
2674 let nested_test = self.nested_test.as_ref().context("no nested test")?;
2675
2676 let test_binary_path =
2677 if let Some(test_content_dir) = self.guest_shares.get(NESTED_CONTENT_TAG) {
2678 let test_content_dir = absolute(&test_content_dir.host_path)?;
2679 let local_test_binary_path = absolute(&nested_test.test_binary)?;
2680
2681 self.convert_guest_path(
2682 local_test_binary_path
2683 .strip_prefix(&test_content_dir)
2684 .context("test binary should be under content dir")?,
2685 )
2686 } else {
2687 self.binary_with_extension(NESTED_TEST_BINARY)
2688 };
2689
2690 let mut cmd = client
2691 .command(self.agent_disk_path(&[NESTED_TEST_CONTENT_DIR.into(), test_binary_path]));
2692 cmd.arg("--ignored")
2693 .arg("--exact")
2694 .arg(&nested_test.test_name)
2695 .env(
2696 VMM_TESTS_CONTENT_DIR,
2697 self.agent_disk_path(&[NESTED_TEST_CONTENT_DIR.into()]),
2698 )
2699 .env(
2700 VMM_TEST_IMAGES,
2701 self.agent_disk_path(&[NESTED_IMAGES_DIR.into()]),
2702 )
2703 .env(
2704 TEST_OUTPUT_PATH,
2705 self.agent_disk_path(&[NESTED_TEST_CONTENT_DIR.into(), NESTED_RESULTS_DIR.into()]),
2706 )
2707 .stdout(crate::pipette::process::Stdio::piped())
2708 .stderr(crate::pipette::process::Stdio::piped());
2709
2710 for (k, v) in env {
2711 cmd.env(k, v);
2712 }
2713
2714 if self.guest_shares.contains_key(NESTED_CONTENT_TAG) {
2715 cmd.env(PETRI_NESTED_TEST_PARENT, &self.name);
2716 }
2717
2718 if let Ok(v) = std::env::var(PETRI_REMOTE_ARTIFACTS) {
2719 cmd.env(PETRI_REMOTE_ARTIFACTS, v);
2720 }
2721
2722 let mut child = cmd.spawn().await?;
2723
2724 self.resources
2726 .driver
2727 .spawn(
2728 "log_nested",
2729 crate::log_task(
2730 self.resources.log_source.log_file("stdout")?,
2731 child.stdout.take().unwrap(),
2732 "nested stdout",
2733 ),
2734 )
2735 .detach();
2736
2737 let output = child.wait_with_output().await?;
2738
2739 if !self.guest_shares.contains_key(NESTED_CONTENT_TAG) {
2741 let test_results_dir = self.agent_disk_path(&[
2742 NESTED_TEST_CONTENT_DIR.into(),
2743 NESTED_RESULTS_DIR.into(),
2744 nested_test.test_name.replace("::", "__"),
2745 ]);
2746
2747 for file in client.list_dir(&test_results_dir).await? {
2748 let data = client
2749 .read_file(self.build_guest_path(&[test_results_dir.clone(), file.clone()]))
2750 .await?;
2751 let mut output = self
2752 .resources
2753 .log_source
2754 .create_attachment_no_prefix(&file)?;
2755 output.write_all(&data)?;
2756 }
2757 }
2758
2759 if !output.status.success() {
2760 anyhow::bail!(
2761 "nested test exited with status {}: {}",
2762 output.status,
2763 String::from_utf8_lossy(&output.stderr)
2764 );
2765 }
2766
2767 Ok(())
2768 }
2769}
2770
2771#[async_trait]
2773pub trait PetriVmRuntime: Send + Sync + 'static {
2774 type VmInspector: PetriVmInspector;
2776 type VmFramebufferAccess: PetriVmFramebufferAccess;
2778
2779 async fn teardown(self) -> anyhow::Result<()>;
2781 async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReasonDetail>;
2784 async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>;
2786 fn openhcl_diag(&self) -> Option<OpenHclDiagHandler>;
2788 async fn wait_for_boot_event(
2791 &mut self,
2792 timeout: Option<Duration>,
2793 ) -> anyhow::Result<Option<FirmwareEvent>>;
2794 async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()>;
2797 async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
2799 async fn restart_openhcl(
2802 &mut self,
2803 new_openhcl: &ResolvedArtifact,
2804 flags: OpenHclServicingFlags,
2805 ) -> anyhow::Result<()>;
2806 async fn save_openhcl(
2810 &mut self,
2811 new_openhcl: &ResolvedArtifact,
2812 flags: OpenHclServicingFlags,
2813 ) -> anyhow::Result<()>;
2814 async fn restore_openhcl(&mut self) -> anyhow::Result<()>;
2817 async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()>;
2820 fn inspector(&self) -> Option<Self::VmInspector> {
2822 None
2823 }
2824 fn take_framebuffer_access(&mut self) -> Option<Self::VmFramebufferAccess> {
2827 None
2828 }
2829 async fn reset(&mut self) -> anyhow::Result<()>;
2831 async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2833 Ok(None)
2834 }
2835 async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()>;
2837 async fn set_vmbus_drive(
2839 &mut self,
2840 disk: &Drive,
2841 controller_id: &Guid,
2842 controller_location: u32,
2843 ) -> anyhow::Result<()>;
2844 async fn add_pcie_device(
2846 &mut self,
2847 port_name: String,
2848 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
2849 ) -> anyhow::Result<()> {
2850 let _ = (port_name, resource);
2851 anyhow::bail!("PCIe hotplug not supported by this backend")
2852 }
2853 async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
2855 let _ = port_name;
2856 anyhow::bail!("PCIe hotplug not supported by this backend")
2857 }
2858}
2859
2860#[async_trait]
2862pub trait PetriVmInspector: Send + Sync + 'static {
2863 async fn inspect(&self, path: &str) -> anyhow::Result<inspect::Node>;
2866}
2867
2868pub struct NoPetriVmInspector;
2870#[async_trait]
2871impl PetriVmInspector for NoPetriVmInspector {
2872 async fn inspect(&self, _path: &str) -> anyhow::Result<inspect::Node> {
2873 unreachable!()
2874 }
2875}
2876
2877pub struct VmScreenshotMeta {
2879 pub color: image::ExtendedColorType,
2881 pub width: u16,
2883 pub height: u16,
2885}
2886
2887#[async_trait]
2889pub trait PetriVmFramebufferAccess: Send + 'static {
2890 async fn screenshot(&mut self, image: &mut Vec<u8>)
2893 -> anyhow::Result<Option<VmScreenshotMeta>>;
2894}
2895
2896pub struct NoPetriVmFramebufferAccess;
2898#[async_trait]
2899impl PetriVmFramebufferAccess for NoPetriVmFramebufferAccess {
2900 async fn screenshot(
2901 &mut self,
2902 _image: &mut Vec<u8>,
2903 ) -> anyhow::Result<Option<VmScreenshotMeta>> {
2904 unreachable!()
2905 }
2906}
2907
2908#[derive(Debug)]
2910pub struct ProcessorTopology {
2911 pub vp_count: u32,
2913 pub enable_smt: Option<bool>,
2915 pub vps_per_socket: Option<u32>,
2917 pub apic_mode: Option<ApicMode>,
2919}
2920
2921impl Default for ProcessorTopology {
2922 fn default() -> Self {
2923 Self {
2924 vp_count: 2,
2925 enable_smt: None,
2926 vps_per_socket: None,
2927 apic_mode: None,
2928 }
2929 }
2930}
2931
2932impl ProcessorTopology {
2933 pub fn heavy() -> Self {
2935 Self {
2936 vp_count: 16,
2937 vps_per_socket: Some(8),
2938 ..Default::default()
2939 }
2940 }
2941
2942 pub fn very_heavy() -> Self {
2944 Self {
2945 vp_count: 32,
2946 vps_per_socket: Some(16),
2947 ..Default::default()
2948 }
2949 }
2950}
2951
2952#[derive(Debug, Clone, Copy)]
2954pub enum ApicMode {
2955 Xapic,
2957 X2apicSupported,
2959 X2apicEnabled,
2961}
2962
2963#[derive(Debug)]
2965pub struct MemoryConfig {
2966 pub startup_bytes: u64,
2969 pub dynamic_memory_range: Option<(u64, u64)>,
2973 pub numa_mem_sizes: Option<Vec<u64>>,
2976 pub private_memory: Option<bool>,
2995 pub transparent_hugepages: bool,
3007}
3008
3009impl Default for MemoryConfig {
3010 fn default() -> Self {
3011 Self {
3012 startup_bytes: 4 * 1024 * 1024 * 1024, dynamic_memory_range: None,
3014 numa_mem_sizes: None,
3015 private_memory: None,
3016 transparent_hugepages: true,
3017 }
3018 }
3019}
3020
3021#[derive(Debug)]
3023pub struct UefiConfig {
3024 pub secure_boot_enabled: bool,
3026 pub secure_boot_template: Option<SecureBootTemplate>,
3028 pub custom_uefi_json: Option<Vec<u8>>,
3030 pub disable_frontpage: bool,
3032 pub default_boot_always_attempt: bool,
3034 pub enable_vpci_boot: bool,
3036 pub force_dma_bounce: bool,
3038 pub efi_diagnostics_log_level: EfiDiagnosticsLogLevel,
3040 pub efi_diagnostics_rate_limit: Option<u32>,
3043}
3044
3045impl Default for UefiConfig {
3046 fn default() -> Self {
3047 Self {
3048 secure_boot_enabled: false,
3049 secure_boot_template: None,
3050 custom_uefi_json: None,
3051 disable_frontpage: true,
3052 default_boot_always_attempt: false,
3053 enable_vpci_boot: false,
3054 force_dma_bounce: false,
3055 efi_diagnostics_log_level: EfiDiagnosticsLogLevel::Default,
3056 efi_diagnostics_rate_limit: None,
3057 }
3058 }
3059}
3060
3061#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
3066pub enum EfiDiagnosticsLogLevel {
3067 #[default]
3069 Default,
3070 Info,
3072 Full,
3074}
3075
3076#[derive(Debug, Clone)]
3078pub enum OpenvmmLogConfig {
3079 TestDefault,
3083 BuiltInDefault,
3086 Custom(BTreeMap<String, String>),
3096}
3097
3098#[derive(Debug)]
3100pub struct OpenHclConfig {
3101 pub vmbus_redirect: bool,
3103 pub enable_mana_keepalive: bool,
3105 pub custom_command_line: Option<String>,
3109 pub log_levels: OpenvmmLogConfig,
3113 pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
3116 pub vtl2_settings: Option<Vtl2Settings>,
3118}
3119
3120impl OpenHclConfig {
3121 pub fn command_line(&self) -> String {
3124 let mut cmdline = self.custom_command_line.clone();
3125
3126 if self.enable_mana_keepalive {
3127 append_cmdline(&mut cmdline, "OPENHCL_MANA_KEEP_ALIVE=host,privatepool");
3128 }
3129
3130 match &self.log_levels {
3131 OpenvmmLogConfig::TestDefault => {
3132 let default_log_levels = {
3133 let openhcl_tracing = if let Ok(x) =
3135 std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG"))
3136 {
3137 format!("OPENVMM_LOG={x}")
3138 } else {
3139 "OPENVMM_LOG=debug".to_owned()
3140 };
3141 let openhcl_show_spans = if let Ok(x) = std::env::var("OPENVMM_SHOW_SPANS") {
3142 format!("OPENVMM_SHOW_SPANS={x}")
3143 } else {
3144 "OPENVMM_SHOW_SPANS=false".to_owned()
3145 };
3146 format!("{openhcl_tracing} {openhcl_show_spans}")
3147 };
3148 append_cmdline(&mut cmdline, &default_log_levels);
3149 }
3150 OpenvmmLogConfig::BuiltInDefault => {
3151 }
3153 OpenvmmLogConfig::Custom(levels) => {
3154 levels.iter().for_each(|(key, value)| {
3155 append_cmdline(&mut cmdline, format!("{key}={value}"));
3156 });
3157 }
3158 }
3159
3160 cmdline.unwrap_or_default()
3161 }
3162}
3163
3164impl Default for OpenHclConfig {
3165 fn default() -> Self {
3166 Self {
3167 vmbus_redirect: false,
3168 enable_mana_keepalive: true,
3169 custom_command_line: None,
3170 log_levels: OpenvmmLogConfig::TestDefault,
3171 vtl2_base_address_type: None,
3172 vtl2_settings: None,
3173 }
3174 }
3175}
3176
3177#[derive(Debug)]
3179pub struct TpmConfig {
3180 pub no_persistent_secrets: bool,
3182 pub hardware_sealing_policy: PetriHardwareSealingPolicy,
3184 pub version: PetriTpmVersion,
3186}
3187
3188impl Default for TpmConfig {
3189 fn default() -> Self {
3190 Self {
3191 no_persistent_secrets: true,
3192 hardware_sealing_policy: PetriHardwareSealingPolicy::Default,
3193 version: PetriTpmVersion::default(),
3194 }
3195 }
3196}
3197
3198#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
3200pub enum PetriTpmVersion {
3201 V138,
3203 #[default]
3205 V185,
3206}
3207
3208impl From<PetriTpmVersion> for tpm_resources::TpmVersion {
3209 fn from(version: PetriTpmVersion) -> Self {
3210 match version {
3211 PetriTpmVersion::V138 => tpm_resources::TpmVersion::V138,
3212 PetriTpmVersion::V185 => tpm_resources::TpmVersion::V185,
3213 }
3214 }
3215}
3216
3217impl From<PetriTpmVersion> for get_resources::ged::GedTpmVersion {
3218 fn from(version: PetriTpmVersion) -> Self {
3219 match version {
3220 PetriTpmVersion::V138 => get_resources::ged::GedTpmVersion::V138,
3221 PetriTpmVersion::V185 => get_resources::ged::GedTpmVersion::V185,
3222 }
3223 }
3224}
3225
3226#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
3231pub enum PetriHardwareSealingPolicy {
3232 #[default]
3234 Default,
3235 HashPolicy,
3237 SignerPolicy,
3239}
3240
3241#[derive(Debug)]
3243pub enum IgvmFirmwareSource {
3244 File(ResolvedArtifact),
3246 VmgsOrInBox,
3248}
3249
3250#[derive(Debug)]
3254pub enum Firmware {
3255 LinuxDirect {
3257 kernel: ResolvedArtifact,
3259 initrd: ResolvedArtifact,
3261 },
3262 IgvmLinuxDirect {
3264 igvm_path: ResolvedArtifact,
3266 isolation: IsolationType,
3268 },
3269 OpenhclLinuxDirect {
3271 igvm_firmware: IgvmFirmwareSource,
3273 openhcl_config: OpenHclConfig,
3275 },
3276 Pcat {
3278 guest: PcatGuest,
3280 bios_firmware: ResolvedOptionalArtifact,
3282 svga_firmware: ResolvedOptionalArtifact,
3284 ide_controllers: [[Option<Drive>; 2]; 2],
3286 },
3287 OpenhclPcat {
3289 guest: PcatGuest,
3291 igvm_firmware: IgvmFirmwareSource,
3293 bios_firmware: ResolvedOptionalArtifact,
3295 svga_firmware: ResolvedOptionalArtifact,
3297 openhcl_config: OpenHclConfig,
3299 },
3300 Uefi {
3302 guest: UefiGuest,
3304 uefi_firmware: ResolvedArtifact,
3306 uefi_config: UefiConfig,
3308 },
3309 OpenhclUefi {
3311 guest: UefiGuest,
3313 isolation: Option<IsolationType>,
3315 igvm_firmware: IgvmFirmwareSource,
3317 uefi_config: UefiConfig,
3319 openhcl_config: OpenHclConfig,
3321 },
3322}
3323
3324#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3326pub enum BootDeviceType {
3327 None,
3329 Ide,
3331 IdeViaScsi,
3333 IdeViaNvme,
3335 Scsi,
3337 ScsiViaScsi,
3339 ScsiViaNvme,
3341 Nvme,
3343 NvmeViaScsi,
3345 NvmeViaNvme,
3347 PcieNvme,
3349 PcieVirtioBlk,
3351}
3352
3353impl BootDeviceType {
3354 fn requires_vtl2(&self) -> bool {
3355 match self {
3356 BootDeviceType::None
3357 | BootDeviceType::Ide
3358 | BootDeviceType::Scsi
3359 | BootDeviceType::Nvme
3360 | BootDeviceType::PcieNvme
3361 | BootDeviceType::PcieVirtioBlk => false,
3362 BootDeviceType::IdeViaScsi
3363 | BootDeviceType::IdeViaNvme
3364 | BootDeviceType::ScsiViaScsi
3365 | BootDeviceType::ScsiViaNvme
3366 | BootDeviceType::NvmeViaScsi
3367 | BootDeviceType::NvmeViaNvme => true,
3368 }
3369 }
3370
3371 fn requires_vpci_boot(&self) -> bool {
3372 matches!(
3373 self,
3374 BootDeviceType::Nvme | BootDeviceType::NvmeViaScsi | BootDeviceType::NvmeViaNvme
3375 )
3376 }
3377
3378 fn requires_vmbus(&self) -> bool {
3379 match self {
3380 BootDeviceType::None
3381 | BootDeviceType::Ide
3382 | BootDeviceType::PcieNvme
3383 | BootDeviceType::PcieVirtioBlk => false,
3384 BootDeviceType::IdeViaScsi
3385 | BootDeviceType::IdeViaNvme
3386 | BootDeviceType::Scsi
3387 | BootDeviceType::ScsiViaScsi
3388 | BootDeviceType::ScsiViaNvme
3389 | BootDeviceType::Nvme
3390 | BootDeviceType::NvmeViaScsi
3391 | BootDeviceType::NvmeViaNvme => true,
3392 }
3393 }
3394}
3395
3396impl Firmware {
3397 pub fn linux_direct(
3400 resolver: &ArtifactResolver<'_>,
3401 arch: MachineArch,
3402 isolation: Option<IsolationType>,
3403 ) -> Self {
3404 use petri_artifacts_vmm_test::artifacts::loadable::*;
3405 use petri_artifacts_vmm_test::artifacts::snp_igvm::SNP_LINUX_DIRECT_IGVM_X64;
3406 match (arch, isolation) {
3407 (MachineArch::X86_64, None) => Firmware::LinuxDirect {
3408 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_X64).erase(),
3409 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
3410 },
3411 (MachineArch::Aarch64, None) => Firmware::LinuxDirect {
3412 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_AARCH64).erase(),
3413 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_AARCH64).erase(),
3414 },
3415 (MachineArch::X86_64, Some(IsolationType::Snp)) => Firmware::IgvmLinuxDirect {
3416 igvm_path: resolver.require(SNP_LINUX_DIRECT_IGVM_X64).erase(),
3417 isolation: IsolationType::Snp,
3418 },
3419 (arch, Some(isolation)) => {
3420 panic!("Linux direct does not support {isolation:?} isolation on {arch:?}")
3421 }
3422 }
3423 }
3424
3425 pub fn linux_direct_bzimage(resolver: &ArtifactResolver<'_>) -> Self {
3430 use petri_artifacts_vmm_test::artifacts::loadable::*;
3431 Firmware::LinuxDirect {
3432 kernel: resolver.require(LINUX_DIRECT_TEST_BZIMAGE_X64).erase(),
3433 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
3434 }
3435 }
3436
3437 pub fn openhcl_linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
3439 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
3440 match arch {
3441 MachineArch::X86_64 => Firmware::OpenhclLinuxDirect {
3442 igvm_firmware: IgvmFirmwareSource::File(
3443 resolver.require(LATEST_LINUX_DIRECT_TEST_X64).erase(),
3444 ),
3445 openhcl_config: Default::default(),
3446 },
3447 MachineArch::Aarch64 => todo!("Linux direct not yet supported on aarch64"),
3448 }
3449 }
3450
3451 pub fn pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
3453 use petri_artifacts_vmm_test::artifacts::loadable::*;
3454 Firmware::Pcat {
3455 guest,
3456 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
3457 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
3458 ide_controllers: [[None, None], [None, None]],
3459 }
3460 }
3461
3462 pub fn openhcl_pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
3464 use petri_artifacts_vmm_test::artifacts::loadable::*;
3465 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
3466 Firmware::OpenhclPcat {
3467 guest,
3468 igvm_firmware: IgvmFirmwareSource::File(resolver.require(LATEST_STANDARD_X64).erase()),
3469 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
3470 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
3471 openhcl_config: OpenHclConfig {
3472 vmbus_redirect: true,
3474 ..Default::default()
3475 },
3476 }
3477 }
3478
3479 pub fn uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch, guest: UefiGuest) -> Self {
3481 use petri_artifacts_vmm_test::artifacts::loadable::*;
3482 let uefi_firmware = match arch {
3483 MachineArch::X86_64 => resolver.require(UEFI_FIRMWARE_X64).erase(),
3484 MachineArch::Aarch64 => resolver.require(UEFI_FIRMWARE_AARCH64).erase(),
3485 };
3486 Firmware::Uefi {
3487 guest,
3488 uefi_firmware,
3489 uefi_config: Default::default(),
3490 }
3491 }
3492
3493 pub fn openhcl_uefi(
3495 resolver: &ArtifactResolver<'_>,
3496 arch: MachineArch,
3497 guest: UefiGuest,
3498 isolation: Option<IsolationType>,
3499 ) -> Self {
3500 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
3501 let igvm_firmware = IgvmFirmwareSource::File(match arch {
3502 MachineArch::X86_64 if isolation.is_some() => resolver.require(LATEST_CVM_X64).erase(),
3503 MachineArch::X86_64 => resolver.require(LATEST_STANDARD_X64).erase(),
3504 MachineArch::Aarch64 => resolver.require(LATEST_STANDARD_AARCH64).erase(),
3505 });
3506 Firmware::OpenhclUefi {
3507 guest,
3508 isolation,
3509 igvm_firmware,
3510 uefi_config: Default::default(),
3511 openhcl_config: Default::default(),
3512 }
3513 }
3514
3515 fn is_openhcl(&self) -> bool {
3516 match self {
3517 Firmware::OpenhclLinuxDirect { .. }
3518 | Firmware::OpenhclUefi { .. }
3519 | Firmware::OpenhclPcat { .. } => true,
3520 Firmware::LinuxDirect { .. }
3521 | Firmware::IgvmLinuxDirect { .. }
3522 | Firmware::Pcat { .. }
3523 | Firmware::Uefi { .. } => false,
3524 }
3525 }
3526
3527 fn isolation(&self) -> Option<IsolationType> {
3528 match self {
3529 Firmware::IgvmLinuxDirect { isolation, .. } => Some(*isolation),
3530 Firmware::OpenhclUefi { isolation, .. } => *isolation,
3531 Firmware::LinuxDirect { .. }
3532 | Firmware::Pcat { .. }
3533 | Firmware::Uefi { .. }
3534 | Firmware::OpenhclLinuxDirect { .. }
3535 | Firmware::OpenhclPcat { .. } => None,
3536 }
3537 }
3538
3539 fn is_isolated_without_paravisor(&self) -> bool {
3541 self.isolation().is_some() && !self.is_openhcl()
3542 }
3543
3544 fn is_linux_direct(&self) -> bool {
3545 match self {
3546 Firmware::LinuxDirect { .. }
3547 | Firmware::IgvmLinuxDirect { .. }
3548 | Firmware::OpenhclLinuxDirect { .. } => true,
3549 Firmware::Pcat { .. }
3550 | Firmware::Uefi { .. }
3551 | Firmware::OpenhclUefi { .. }
3552 | Firmware::OpenhclPcat { .. } => false,
3553 }
3554 }
3555
3556 pub fn linux_direct_initrd(&self) -> Option<&Path> {
3558 match self {
3559 Firmware::LinuxDirect { initrd, .. } => Some(initrd.get()),
3560 _ => None,
3561 }
3562 }
3563
3564 fn is_pcat(&self) -> bool {
3565 match self {
3566 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => true,
3567 Firmware::Uefi { .. }
3568 | Firmware::OpenhclUefi { .. }
3569 | Firmware::LinuxDirect { .. }
3570 | Firmware::IgvmLinuxDirect { .. }
3571 | Firmware::OpenhclLinuxDirect { .. } => false,
3572 }
3573 }
3574
3575 fn os_flavor(&self) -> OsFlavor {
3576 match self {
3577 Firmware::LinuxDirect { .. }
3578 | Firmware::OpenhclLinuxDirect { .. }
3579 | Firmware::Uefi {
3580 guest: UefiGuest::Mshv(_),
3581 ..
3582 }
3583 | Firmware::OpenhclUefi {
3584 guest: UefiGuest::Mshv(_),
3585 ..
3586 }
3587 | Firmware::IgvmLinuxDirect { .. } => OsFlavor::Linux,
3588 Firmware::Uefi {
3589 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
3590 ..
3591 }
3592 | Firmware::OpenhclUefi {
3593 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
3594 ..
3595 } => OsFlavor::Uefi,
3596 Firmware::Pcat {
3597 guest: PcatGuest::Vhd(cfg),
3598 ..
3599 }
3600 | Firmware::OpenhclPcat {
3601 guest: PcatGuest::Vhd(cfg),
3602 ..
3603 }
3604 | Firmware::Uefi {
3605 guest: UefiGuest::Vhd(cfg),
3606 ..
3607 }
3608 | Firmware::OpenhclUefi {
3609 guest: UefiGuest::Vhd(cfg),
3610 ..
3611 } => cfg.os_flavor,
3612 Firmware::Pcat {
3613 guest: PcatGuest::Iso(cfg),
3614 ..
3615 }
3616 | Firmware::OpenhclPcat {
3617 guest: PcatGuest::Iso(cfg),
3618 ..
3619 } => cfg.os_flavor,
3620 }
3621 }
3622
3623 fn quirks(&self) -> GuestQuirks {
3624 match self {
3625 Firmware::Pcat {
3626 guest: PcatGuest::Vhd(cfg),
3627 ..
3628 }
3629 | Firmware::Uefi {
3630 guest: UefiGuest::Vhd(cfg),
3631 ..
3632 }
3633 | Firmware::OpenhclUefi {
3634 guest: UefiGuest::Vhd(cfg),
3635 ..
3636 } => cfg.quirks.clone(),
3637 Firmware::Pcat {
3638 guest: PcatGuest::Iso(cfg),
3639 ..
3640 } => cfg.quirks.clone(),
3641 _ => Default::default(),
3642 }
3643 }
3644
3645 fn expected_boot_event(&self) -> Option<FirmwareEvent> {
3646 match self {
3647 Firmware::LinuxDirect { .. }
3648 | Firmware::IgvmLinuxDirect { .. }
3649 | Firmware::OpenhclLinuxDirect { .. }
3650 | Firmware::Uefi {
3651 guest: UefiGuest::GuestTestUefi(_),
3652 ..
3653 }
3654 | Firmware::OpenhclUefi {
3655 guest: UefiGuest::GuestTestUefi(_),
3656 ..
3657 } => None,
3658 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => {
3659 Some(FirmwareEvent::BootAttempt)
3661 }
3662 Firmware::Uefi {
3663 guest: UefiGuest::None,
3664 ..
3665 }
3666 | Firmware::OpenhclUefi {
3667 guest: UefiGuest::None,
3668 ..
3669 } => Some(FirmwareEvent::NoBootDevice),
3670 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => {
3671 Some(FirmwareEvent::BootSuccess)
3672 }
3673 }
3674 }
3675
3676 fn openhcl_config(&self) -> Option<&OpenHclConfig> {
3677 match self {
3678 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
3679 | Firmware::OpenhclUefi { openhcl_config, .. }
3680 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
3681 Firmware::LinuxDirect { .. }
3682 | Firmware::IgvmLinuxDirect { .. }
3683 | Firmware::Pcat { .. }
3684 | Firmware::Uefi { .. } => None,
3685 }
3686 }
3687
3688 fn openhcl_config_mut(&mut self) -> Option<&mut OpenHclConfig> {
3689 match self {
3690 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
3691 | Firmware::OpenhclUefi { openhcl_config, .. }
3692 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
3693 Firmware::LinuxDirect { .. }
3694 | Firmware::IgvmLinuxDirect { .. }
3695 | Firmware::Pcat { .. }
3696 | Firmware::Uefi { .. } => None,
3697 }
3698 }
3699
3700 fn openhcl_firmware(&self) -> Option<&IgvmFirmwareSource> {
3701 match self {
3702 Firmware::OpenhclLinuxDirect { igvm_firmware, .. }
3703 | Firmware::OpenhclUefi { igvm_firmware, .. }
3704 | Firmware::OpenhclPcat { igvm_firmware, .. } => Some(igvm_firmware),
3705 Firmware::LinuxDirect { .. }
3706 | Firmware::IgvmLinuxDirect { .. }
3707 | Firmware::Pcat { .. }
3708 | Firmware::Uefi { .. } => None,
3709 }
3710 }
3711
3712 fn into_runtime_config(
3713 self,
3714 vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
3715 ) -> PetriVmRuntimeConfig {
3716 match self {
3717 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
3718 | Firmware::OpenhclUefi { openhcl_config, .. }
3719 | Firmware::OpenhclPcat { openhcl_config, .. } => PetriVmRuntimeConfig {
3720 vtl2_settings: Some(
3721 openhcl_config
3722 .vtl2_settings
3723 .unwrap_or_else(default_vtl2_settings),
3724 ),
3725 ide_controllers: None,
3726 vmbus_storage_controllers,
3727 },
3728 Firmware::Pcat {
3729 ide_controllers, ..
3730 } => PetriVmRuntimeConfig {
3731 vtl2_settings: None,
3732 ide_controllers: Some(ide_controllers),
3733 vmbus_storage_controllers,
3734 },
3735 Firmware::LinuxDirect { .. }
3736 | Firmware::IgvmLinuxDirect { .. }
3737 | Firmware::Uefi { .. } => PetriVmRuntimeConfig {
3738 vtl2_settings: None,
3739 ide_controllers: None,
3740 vmbus_storage_controllers,
3741 },
3742 }
3743 }
3744
3745 fn uefi_config(&self) -> Option<&UefiConfig> {
3746 match self {
3747 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
3748 Some(uefi_config)
3749 }
3750 Firmware::LinuxDirect { .. }
3751 | Firmware::IgvmLinuxDirect { .. }
3752 | Firmware::OpenhclLinuxDirect { .. }
3753 | Firmware::Pcat { .. }
3754 | Firmware::OpenhclPcat { .. } => None,
3755 }
3756 }
3757
3758 fn uefi_config_mut(&mut self) -> Option<&mut UefiConfig> {
3759 match self {
3760 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
3761 Some(uefi_config)
3762 }
3763 Firmware::LinuxDirect { .. }
3764 | Firmware::IgvmLinuxDirect { .. }
3765 | Firmware::OpenhclLinuxDirect { .. }
3766 | Firmware::Pcat { .. }
3767 | Firmware::OpenhclPcat { .. } => None,
3768 }
3769 }
3770
3771 fn boot_drive(&self) -> anyhow::Result<Option<Drive>> {
3772 if let Firmware::Uefi {
3773 guest: UefiGuest::Mshv(boot),
3774 ..
3775 }
3776 | Firmware::OpenhclUefi {
3777 guest: UefiGuest::Mshv(boot),
3778 ..
3779 } = self
3780 {
3781 return Ok(Some(Drive::new(
3782 Some(boot.build().context("composing mshv boot disk")?),
3783 false,
3784 )));
3785 }
3786 Ok(match self {
3787 Firmware::LinuxDirect { .. }
3788 | Firmware::IgvmLinuxDirect { .. }
3789 | Firmware::OpenhclLinuxDirect { .. } => None,
3790 Firmware::Pcat { guest, .. } | Firmware::OpenhclPcat { guest, .. } => {
3791 Some((guest.disk_path(), guest.is_dvd()))
3792 }
3793 Firmware::Uefi { guest, .. } | Firmware::OpenhclUefi { guest, .. } => {
3794 guest.disk_path().map(|dp| (dp, false))
3795 }
3796 }
3797 .map(|(disk_path, is_dvd)| Drive::new(Some(Disk::Differencing(disk_path)), is_dvd)))
3798 }
3799
3800 fn vtl2_settings(&mut self) -> Option<&mut Vtl2Settings> {
3801 self.openhcl_config_mut()
3802 .map(|c| c.vtl2_settings.get_or_insert_with(default_vtl2_settings))
3803 }
3804
3805 fn ide_controllers(&self) -> Option<&[[Option<Drive>; 2]; 2]> {
3806 match self {
3807 Firmware::Pcat {
3808 ide_controllers, ..
3809 } => Some(ide_controllers),
3810 _ => None,
3811 }
3812 }
3813
3814 fn ide_controllers_mut(&mut self) -> Option<&mut [[Option<Drive>; 2]; 2]> {
3815 match self {
3816 Firmware::Pcat {
3817 ide_controllers, ..
3818 } => Some(ide_controllers),
3819 _ => None,
3820 }
3821 }
3822}
3823
3824#[derive(Debug)]
3827pub enum PcatGuest {
3828 Vhd(BootImageConfig<boot_image_type::Vhd>),
3830 Iso(BootImageConfig<boot_image_type::Iso>),
3832}
3833
3834impl PcatGuest {
3835 fn disk_path(&self) -> DiskPath {
3836 match self {
3837 PcatGuest::Vhd(disk) => disk.disk_path(),
3838 PcatGuest::Iso(disk) => disk.disk_path(),
3839 }
3840 }
3841
3842 fn is_dvd(&self) -> bool {
3843 matches!(self, Self::Iso(_))
3844 }
3845}
3846
3847#[derive(Debug)]
3850pub enum UefiGuest {
3851 Mshv(MshvBoot),
3854 Vhd(BootImageConfig<boot_image_type::Vhd>),
3856 GuestTestUefi(ResolvedArtifact),
3858 None,
3860}
3861
3862impl UefiGuest {
3863 pub fn guest_test_uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
3865 use petri_artifacts_vmm_test::artifacts::test_vhd::*;
3866 let artifact = match arch {
3867 MachineArch::X86_64 => resolver.require(GUEST_TEST_UEFI_X64).erase(),
3868 MachineArch::Aarch64 => resolver.require(GUEST_TEST_UEFI_AARCH64).erase(),
3869 };
3870 UefiGuest::GuestTestUefi(artifact)
3871 }
3872
3873 fn disk_path(&self) -> Option<DiskPath> {
3874 match self {
3875 UefiGuest::Vhd(vhd) => Some(vhd.disk_path()),
3876 UefiGuest::GuestTestUefi(p) => Some(DiskPath::Local(p.get().to_path_buf())),
3877 UefiGuest::None => None,
3878 UefiGuest::Mshv(_) => None,
3879 }
3880 }
3881}
3882
3883pub mod boot_image_type {
3885 mod private {
3886 pub trait Sealed {}
3887 impl Sealed for super::Vhd {}
3888 impl Sealed for super::Iso {}
3889 }
3890
3891 pub trait BootImageType: private::Sealed {}
3894
3895 #[derive(Debug)]
3897 pub enum Vhd {}
3898
3899 #[derive(Debug)]
3901 pub enum Iso {}
3902
3903 impl BootImageType for Vhd {}
3904 impl BootImageType for Iso {}
3905}
3906
3907#[derive(Debug)]
3909pub struct BootImageConfig<T: boot_image_type::BootImageType> {
3910 artifact: ResolvedArtifactSource,
3912 os_flavor: OsFlavor,
3914 quirks: GuestQuirks,
3918 _type: core::marker::PhantomData<T>,
3920}
3921
3922impl<T: boot_image_type::BootImageType> BootImageConfig<T> {
3923 fn disk_path(&self) -> DiskPath {
3925 match self.artifact.get() {
3926 ArtifactSource::Local(p) => DiskPath::Local(p.clone()),
3927 ArtifactSource::Remote { url } => DiskPath::Remote { url: url.clone() },
3928 }
3929 }
3930}
3931
3932impl BootImageConfig<boot_image_type::Vhd> {
3933 pub fn from_vhd<A>(artifact: ResolvedArtifactSource<A>) -> Self
3935 where
3936 A: petri_artifacts_common::tags::IsTestVhd,
3937 {
3938 BootImageConfig {
3939 artifact: artifact.erase(),
3940 os_flavor: A::OS_FLAVOR,
3941 quirks: A::quirks(),
3942 _type: std::marker::PhantomData,
3943 }
3944 }
3945}
3946
3947impl BootImageConfig<boot_image_type::Iso> {
3948 pub fn from_iso<A>(artifact: ResolvedArtifactSource<A>) -> Self
3950 where
3951 A: petri_artifacts_common::tags::IsTestIso,
3952 {
3953 BootImageConfig {
3954 artifact: artifact.erase(),
3955 os_flavor: A::OS_FLAVOR,
3956 quirks: A::quirks(),
3957 _type: std::marker::PhantomData,
3958 }
3959 }
3960}
3961
3962#[derive(Debug, Clone, Copy)]
3964pub enum IsolationType {
3965 Vbs,
3967 Snp,
3969 Tdx,
3971}
3972
3973#[derive(Debug, Clone, Copy)]
3975pub struct OpenHclServicingFlags {
3976 pub enable_nvme_keepalive: bool,
3979 pub enable_mana_keepalive: bool,
3981 pub override_version_checks: bool,
3983 pub stop_timeout_hint_secs: Option<u16>,
3985}
3986
3987#[derive(Debug, Clone)]
3989pub enum DiskPath {
3990 Local(PathBuf),
3992 Remote {
3994 url: String,
3996 },
3997}
3998
3999impl From<PathBuf> for DiskPath {
4000 fn from(path: PathBuf) -> Self {
4001 DiskPath::Local(path)
4002 }
4003}
4004
4005#[derive(Debug, Clone)]
4007pub enum Disk {
4008 Memory(u64),
4010 Differencing(DiskPath),
4012 Persistent(PathBuf),
4014 Temporary(Arc<TempPath>),
4016}
4017
4018#[derive(Debug, Clone)]
4020pub enum TempOrPersistentPath {
4021 Temp(Arc<TempPath>),
4023 Persistent(PathBuf),
4025}
4026
4027impl AsRef<Path> for TempOrPersistentPath {
4028 fn as_ref(&self) -> &Path {
4029 match self {
4030 TempOrPersistentPath::Temp(temp_path) => temp_path.as_ref(),
4031 TempOrPersistentPath::Persistent(path_buf) => path_buf.as_ref(),
4032 }
4033 }
4034}
4035
4036#[derive(Debug, Clone)]
4038pub struct PetriInitrd {
4039 pub path: TempOrPersistentPath,
4041 pub rdinit_param: String,
4043}
4044
4045#[derive(Debug, Clone)]
4047pub struct PetriVmgsDisk {
4048 pub disk: Disk,
4050 pub encryption_policy: GuestStateEncryptionPolicy,
4052}
4053
4054impl Default for PetriVmgsDisk {
4055 fn default() -> Self {
4056 PetriVmgsDisk {
4057 disk: Disk::Memory(vmgs_format::VMGS_DEFAULT_CAPACITY),
4058 encryption_policy: GuestStateEncryptionPolicy::None(false),
4060 }
4061 }
4062}
4063
4064#[derive(Debug, Clone)]
4066pub enum PetriVmgsResource {
4067 Disk(PetriVmgsDisk),
4069 ReprovisionOnFailure(PetriVmgsDisk),
4071 Reprovision(PetriVmgsDisk),
4073 Ephemeral,
4075}
4076
4077impl PetriVmgsResource {
4078 pub fn vmgs(&self) -> Option<&PetriVmgsDisk> {
4080 match self {
4081 PetriVmgsResource::Disk(vmgs)
4082 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
4083 | PetriVmgsResource::Reprovision(vmgs) => Some(vmgs),
4084 PetriVmgsResource::Ephemeral => None,
4085 }
4086 }
4087
4088 pub fn disk(&self) -> Option<&Disk> {
4090 self.vmgs().map(|vmgs| &vmgs.disk)
4091 }
4092
4093 pub fn encryption_policy(&self) -> Option<GuestStateEncryptionPolicy> {
4095 self.vmgs().map(|vmgs| vmgs.encryption_policy)
4096 }
4097}
4098
4099#[derive(Debug, Clone, Copy)]
4101pub enum PetriGuestStateLifetime {
4102 Disk,
4105 ReprovisionOnFailure,
4107 Reprovision,
4109 Ephemeral,
4111}
4112
4113#[derive(Debug, Clone, Copy)]
4115pub enum SecureBootTemplate {
4116 MicrosoftWindows,
4118 MicrosoftUefiCertificateAuthority,
4120}
4121
4122#[derive(Default, Debug, Clone)]
4125pub struct VmmQuirks {
4126 pub flaky_boot: Option<Duration>,
4129}
4130
4131#[derive(Debug, Clone, Copy, Eq, PartialEq)]
4133pub enum PetriHaltReason {
4134 PowerOff,
4136 Reset,
4138 Hibernate,
4140 TripleFault,
4142 Other,
4144}
4145
4146impl PetriHaltReason {
4147 pub fn with_detail(self, detail: String) -> PetriHaltReasonDetail {
4149 PetriHaltReasonDetail {
4150 reason: self,
4151 detail,
4152 }
4153 }
4154}
4155
4156#[derive(Debug, Clone)]
4158pub struct PetriHaltReasonDetail {
4159 pub reason: PetriHaltReason,
4161 pub detail: String,
4163}
4164
4165fn append_cmdline(cmd: &mut Option<String>, add_cmd: impl AsRef<str>) {
4166 if let Some(cmd) = cmd.as_mut() {
4167 cmd.push(' ');
4168 cmd.push_str(add_cmd.as_ref());
4169 } else {
4170 *cmd = Some(add_cmd.as_ref().to_string());
4171 }
4172}
4173
4174async fn collect_inspect(
4175 name: &'static str,
4176 inspect: impl Future<Output = anyhow::Result<inspect::Node>>,
4177 log_source: &PetriLogSource,
4178 prefix: &str,
4179) {
4180 let node = match CancelContext::new()
4181 .with_timeout(INSPECT_TIMEOUT)
4182 .until_cancelled(inspect)
4183 .await
4184 {
4185 Ok(Ok(node)) => node,
4186 Ok(Err(e)) => {
4187 tracing::warn!(name, ?e, "failed to collect inspect state");
4188 return;
4189 }
4190 Err(_) => {
4191 tracing::warn!(name, "timed out collecting inspect state");
4192 return;
4193 }
4194 };
4195
4196 if let Err(e) = log_source.write_attachment(
4197 &format!("{prefix}_inspect_{name}.log"),
4198 format!("{node:#}").as_bytes(),
4199 ) {
4200 tracing::error!(name, ?e, "failed to save inspect log");
4201 }
4202}
4203
4204pub struct ModifyFn<T>(pub Box<dyn FnOnce(T) -> T + Send>);
4206
4207impl<T> Debug for ModifyFn<T> {
4208 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4209 write!(f, "_")
4210 }
4211}
4212
4213fn default_vtl2_settings() -> Vtl2Settings {
4215 Vtl2Settings {
4216 version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
4217 fixed: None,
4218 dynamic: Some(Default::default()),
4219 namespace_settings: Default::default(),
4220 }
4221}
4222
4223#[derive(Debug, Copy, Clone, PartialEq, Eq)]
4225pub enum Vtl {
4226 Vtl0 = 0,
4228 Vtl1 = 1,
4230 Vtl2 = 2,
4232}
4233
4234#[derive(Debug, Copy, Clone, PartialEq, Eq)]
4236pub enum VmbusStorageType {
4237 Scsi,
4239 Nvme,
4241 VirtioBlk,
4243}
4244
4245#[derive(Debug, Clone)]
4247pub struct Drive {
4248 pub disk: Option<Disk>,
4250 pub is_dvd: bool,
4252}
4253
4254impl Drive {
4255 pub fn new(disk: Option<Disk>, is_dvd: bool) -> Self {
4257 Self { disk, is_dvd }
4258 }
4259}
4260
4261#[derive(Debug, Clone)]
4263pub struct VmbusStorageController {
4264 pub target_vtl: Vtl,
4266 pub controller_type: VmbusStorageType,
4268 pub drives: HashMap<u32, Drive>,
4270}
4271
4272impl VmbusStorageController {
4273 pub fn new(target_vtl: Vtl, controller_type: VmbusStorageType) -> Self {
4275 Self {
4276 target_vtl,
4277 controller_type,
4278 drives: HashMap::new(),
4279 }
4280 }
4281
4282 pub fn set_drive(
4284 &mut self,
4285 lun: Option<u32>,
4286 drive: Drive,
4287 allow_modify_existing: bool,
4288 ) -> u32 {
4289 let lun = lun.unwrap_or_else(|| {
4290 let mut lun = None;
4292 for x in 0..u8::MAX as u32 {
4293 if !self.drives.contains_key(&x) {
4294 lun = Some(x);
4295 break;
4296 }
4297 }
4298 lun.expect("all locations on this controller are in use")
4299 });
4300
4301 if self.drives.insert(lun, drive).is_some() && !allow_modify_existing {
4302 panic!("a disk with lun {lun} already existed on this controller");
4303 }
4304
4305 lun
4306 }
4307}
4308
4309pub(crate) fn petri_disk_cache_dir() -> String {
4311 if let Ok(dir) = std::env::var("PETRI_CACHE_DIR") {
4312 return dir;
4313 }
4314
4315 #[cfg(target_os = "macos")]
4316 {
4317 if let Ok(home) = std::env::var("HOME") {
4318 return format!("{home}/Library/Caches/petri");
4319 }
4320 }
4321
4322 #[cfg(windows)]
4323 {
4324 if let Ok(local) = std::env::var("LOCALAPPDATA") {
4325 return format!("{local}\\petri\\cache");
4326 }
4327 }
4328
4329 if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
4331 return format!("{xdg}/petri");
4332 }
4333 if let Ok(home) = std::env::var("HOME") {
4334 return format!("{home}/.cache/petri");
4335 }
4336
4337 ".cache/petri".to_string()
4338}
4339
4340pub struct NestedTestDeps {
4342 pub archive: ErasedArtifactHandle,
4344 pub binary: String,
4346 pub test_name: String,
4348}
4349
4350impl NestedTestDeps {
4351 pub fn new<A: IsNextestArchive>(
4353 archive: ArtifactHandle<A>,
4354 binary: &str,
4355 test_name: &str,
4356 ) -> Self {
4357 Self {
4358 archive: archive.erase(),
4359 binary: binary.into(),
4360 test_name: test_name.into(),
4361 }
4362 }
4363}
4364
4365pub struct ResolvedNestedTestDeps {
4367 pub test_binary: TempPath,
4369 pub test_name: String,
4371 pub artifacts: Vec<(ErasedArtifactHandle, PathBuf)>,
4373}
4374
4375mod nextest_binaries_metadata_schema {
4376 use serde::Deserialize;
4377 use serde::Serialize;
4378 use std::collections::HashMap;
4379 use std::path::PathBuf;
4380
4381 #[derive(Serialize, Deserialize)]
4382 #[serde(rename_all = "kebab-case")]
4383 pub struct RustBuildMeta {
4384 pub target_directory: PathBuf,
4385 }
4386
4387 #[derive(Serialize, Deserialize)]
4388 #[serde(rename_all = "kebab-case")]
4389 pub struct Binary {
4390 pub binary_name: String,
4391 pub binary_path: PathBuf,
4392 }
4393
4394 #[derive(Serialize, Deserialize)]
4395 #[serde(rename_all = "kebab-case")]
4396 pub struct BinariesMetadata {
4397 pub rust_build_meta: RustBuildMeta,
4398 pub rust_binaries: HashMap<String, Binary>,
4399 }
4400}
4401
4402impl NestedTestDeps {
4403 fn extract_binary(&self) -> anyhow::Result<TempPath> {
4405 let temp_dir = tempfile::tempdir()?;
4406
4407 let archive = resolve_artifact(self.archive)?;
4410
4411 let mut cmd = std::process::Command::new("tar");
4412 cmd.arg("--zstd")
4413 .arg("-xvf")
4414 .arg(archive)
4415 .current_dir(&temp_dir);
4416 futures::executor::block_on(crate::run_host_cmd(cmd))?;
4417
4418 let meta_file = fs_err::File::open(
4419 temp_dir
4420 .path()
4421 .join("target")
4422 .join("nextest")
4423 .join("binaries-metadata.json"),
4424 )?;
4425 let meta: nextest_binaries_metadata_schema::BinariesMetadata =
4426 serde_json::from_reader(meta_file)?;
4427
4428 let build_binary_path = meta
4429 .rust_binaries
4430 .iter()
4431 .find(|(_, b)| b.binary_name == self.binary)
4432 .context("binary missing")?
4433 .1
4434 .binary_path
4435 .clone();
4436
4437 let local_binary_path = temp_dir.path().join(
4438 build_binary_path
4439 .strip_prefix(meta.rust_build_meta.target_directory.parent().unwrap())?,
4440 );
4441
4442 let test_content_temp_dir =
4444 PathBuf::from(std::env::var(VMM_TESTS_CONTENT_DIR)?).join(NESTED_TEMP_DIR);
4445 fs_err::create_dir_all(&test_content_temp_dir)?;
4446 let temp_path = tempfile::NamedTempFile::new_in(&test_content_temp_dir)
4447 .context("failed to create temp nested test")?
4448 .into_temp_path();
4449
4450 fs_err::copy(&local_binary_path, &temp_path)?;
4451
4452 Ok(temp_path)
4453 }
4454
4455 fn resolve(&self, discover_artifacts: bool) -> anyhow::Result<ResolvedNestedTestDeps> {
4456 let test_binary = self
4457 .extract_binary()
4458 .context("failed to extract nextest binary")?;
4459
4460 let artifacts = if discover_artifacts {
4461 petri_artifacts_core::query_test_binary_artifacts(&test_binary, &[&self.test_name])
4462 .context("failed to query test binary for artifacts")?
4463 .into_artifacts_list()
4464 .into_iter()
4465 .filter(|id| {
4466 id != petri_artifacts_common::artifacts::TEST_LOG_DIRECTORY::GLOBAL_UNIQUE_ID
4467 })
4468 .map(|id| {
4469 let handle = petri_artifacts_core::artifact_from_id(&id)
4470 .with_context(|| format!("unknown artifact handle: {id}"))?;
4471 Ok((handle, resolve_artifact(handle)?))
4472 })
4473 .collect::<anyhow::Result<Vec<_>>>()?
4474 } else {
4475 Vec::new()
4476 };
4477
4478 Ok(ResolvedNestedTestDeps {
4479 test_binary,
4480 test_name: self.test_name.clone(),
4481 artifacts,
4482 })
4483 }
4484}
4485
4486trait PetriGuestPaths {
4487 fn os_flavor(&self) -> OsFlavor;
4489
4490 fn path_separator(&self) -> &'static str {
4492 match self.os_flavor() {
4493 OsFlavor::Windows => "\\",
4494 _ => "/",
4495 }
4496 }
4497
4498 fn agent_disk_root(&self) -> &'static str {
4502 match self.os_flavor() {
4503 OsFlavor::Windows => "D:",
4504 _ => "/cidata",
4505 }
4506 }
4507
4508 fn build_guest_path(&self, components: &[String]) -> String {
4511 components.join(self.path_separator())
4512 }
4513
4514 fn convert_guest_path(&self, path: &Path) -> String {
4517 self.build_guest_path(
4518 &path
4519 .iter()
4520 .map(|c| c.to_str().expect("path not utf8").to_string())
4521 .collect::<Vec<_>>(),
4522 )
4523 }
4524
4525 fn parents<'a>(&self, path: &'a str) -> Vec<&'a str> {
4527 path.match_indices(self.path_separator())
4528 .map(|(x, _)| &path[0..x])
4529 .collect()
4530 }
4531
4532 fn agent_disk_path(&self, components: &[String]) -> String {
4536 self.build_guest_path(&[&[self.agent_disk_root().into()], components].concat())
4537 }
4538
4539 fn binary_with_extension(&self, name: &str) -> String {
4541 match self.os_flavor() {
4542 OsFlavor::Windows => format!("{name}.exe"),
4543 _ => name.into(),
4544 }
4545 }
4546}
4547
4548struct FatFsPathBuilder(OsFlavor);
4549
4550impl PetriGuestPaths for FatFsPathBuilder {
4551 fn os_flavor(&self) -> OsFlavor {
4552 self.0
4553 }
4554
4555 fn path_separator(&self) -> &'static str {
4556 "/"
4557 }
4558}
4559
4560impl<T: PetriVmmBackend> PetriGuestPaths for PetriVm<T> {
4561 fn os_flavor(&self) -> OsFlavor {
4562 self.properties.os_flavor
4563 }
4564}
4565
4566impl<T: PetriVmmBackend> PetriGuestPaths for PetriVmBuilder<T> {
4567 fn os_flavor(&self) -> OsFlavor {
4568 self.os_flavor()
4569 }
4570}
4571
4572#[cfg(test)]
4573mod tests {
4574 use super::*;
4575 use crate::Drive;
4576 use crate::VmbusStorageController;
4577 use crate::VmbusStorageType;
4578 use crate::Vtl;
4579
4580 #[derive(Clone, Copy)]
4581 enum TestInspectBehavior {
4582 Success,
4583 Error,
4584 Panic,
4585 }
4586
4587 #[derive(Clone)]
4588 struct TestInspector(TestInspectBehavior);
4589
4590 #[async_trait::async_trait]
4591 impl PetriVmInspector for TestInspector {
4592 async fn inspect(&self, _path: &str) -> anyhow::Result<inspect::Node> {
4593 match self.0 {
4594 TestInspectBehavior::Success => Ok(inspect::Node::Unevaluated),
4595 TestInspectBehavior::Error => anyhow::bail!("inspect failed"),
4596 TestInspectBehavior::Panic => panic!("inspect panicked"),
4597 }
4598 }
4599 }
4600
4601 struct TestFramebuffer;
4602
4603 #[async_trait::async_trait]
4604 impl PetriVmFramebufferAccess for TestFramebuffer {
4605 async fn screenshot(
4606 &mut self,
4607 _image: &mut Vec<u8>,
4608 ) -> anyhow::Result<Option<VmScreenshotMeta>> {
4609 unreachable!()
4610 }
4611 }
4612
4613 struct TestRuntime(TestInspectBehavior);
4614
4615 #[async_trait::async_trait]
4616 impl PetriVmRuntime for TestRuntime {
4617 type VmInspector = TestInspector;
4618 type VmFramebufferAccess = TestFramebuffer;
4619
4620 async fn teardown(self) -> anyhow::Result<()> {
4621 Ok(())
4622 }
4623
4624 async fn wait_for_halt(
4625 &mut self,
4626 _allow_reset: bool,
4627 ) -> anyhow::Result<PetriHaltReasonDetail> {
4628 unreachable!()
4629 }
4630
4631 async fn wait_for_agent(&mut self, _set_high_vtl: bool) -> anyhow::Result<PipetteClient> {
4632 unreachable!()
4633 }
4634
4635 fn openhcl_diag(&self) -> Option<OpenHclDiagHandler> {
4636 None
4637 }
4638
4639 async fn wait_for_boot_event(
4640 &mut self,
4641 _timeout: Option<Duration>,
4642 ) -> anyhow::Result<Option<FirmwareEvent>> {
4643 unreachable!()
4644 }
4645
4646 async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()> {
4647 unreachable!()
4648 }
4649
4650 async fn send_enlightened_shutdown(&mut self, _kind: ShutdownKind) -> anyhow::Result<()> {
4651 unreachable!()
4652 }
4653
4654 async fn restart_openhcl(
4655 &mut self,
4656 _new_openhcl: &ResolvedArtifact,
4657 _flags: OpenHclServicingFlags,
4658 ) -> anyhow::Result<()> {
4659 unreachable!()
4660 }
4661
4662 async fn save_openhcl(
4663 &mut self,
4664 _new_openhcl: &ResolvedArtifact,
4665 _flags: OpenHclServicingFlags,
4666 ) -> anyhow::Result<()> {
4667 unreachable!()
4668 }
4669
4670 async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
4671 unreachable!()
4672 }
4673
4674 async fn update_command_line(&mut self, _command_line: &str) -> anyhow::Result<()> {
4675 unreachable!()
4676 }
4677
4678 fn inspector(&self) -> Option<Self::VmInspector> {
4679 Some(TestInspector(self.0))
4680 }
4681
4682 async fn reset(&mut self) -> anyhow::Result<()> {
4683 unreachable!()
4684 }
4685
4686 async fn set_vtl2_settings(&mut self, _settings: &Vtl2Settings) -> anyhow::Result<()> {
4687 unreachable!()
4688 }
4689
4690 async fn set_vmbus_drive(
4691 &mut self,
4692 _disk: &Drive,
4693 _controller_id: &Guid,
4694 _controller_location: u32,
4695 ) -> anyhow::Result<()> {
4696 unreachable!()
4697 }
4698 }
4699
4700 fn test_runtime_guard(
4701 driver: DefaultDriver,
4702 log_source: &PetriLogSource,
4703 behavior: TestInspectBehavior,
4704 ) -> PetriVmRuntimeGuard<TestRuntime> {
4705 PetriVmRuntimeGuard {
4706 runtime: Some(TestRuntime(behavior)),
4707 driver,
4708 log_source: log_source.clone(),
4709 capture_inspect_on_drop: true,
4710 }
4711 }
4712
4713 fn run_failed_test(
4714 log_source: &PetriLogSource,
4715 behavior: TestInspectBehavior,
4716 ) -> anyhow::Result<()> {
4717 let pool = pal_async::DefaultPool::new();
4718 let driver = pool.driver();
4719 drop(test_runtime_guard(driver.clone(), log_source, behavior));
4720 drop(driver);
4721 pool.run();
4722 anyhow::bail!("original test failure")
4723 }
4724
4725 fn inspect_attachment_count(log_source: &PetriLogSource) -> usize {
4726 fs_err::read_dir(log_source.output_dir())
4727 .unwrap()
4728 .filter_map(Result::ok)
4729 .filter(|entry| {
4730 entry
4731 .file_name()
4732 .to_string_lossy()
4733 .starts_with("failure_inspect_vmm")
4734 })
4735 .count()
4736 }
4737
4738 #[test]
4739 fn test_runtime_guard_failure_capture() {
4740 let temp_dir = tempfile::tempdir().unwrap();
4741 let log_source = crate::tracing::try_init_tracing(
4742 temp_dir.path(),
4743 tracing::level_filters::LevelFilter::DEBUG,
4744 None,
4745 )
4746 .unwrap();
4747
4748 let error = run_failed_test(&log_source, TestInspectBehavior::Success).unwrap_err();
4749 assert_eq!(error.to_string(), "original test failure");
4750 assert_eq!(inspect_attachment_count(&log_source), 1);
4751
4752 let pool = pal_async::DefaultPool::new();
4753 let driver = pool.driver();
4754 let mut guard =
4755 test_runtime_guard(driver.clone(), &log_source, TestInspectBehavior::Success);
4756 let _runtime = guard.take_for_teardown();
4757 drop(guard);
4758 drop(driver);
4759 pool.run();
4760 assert_eq!(inspect_attachment_count(&log_source), 1);
4761
4762 for behavior in [TestInspectBehavior::Error, TestInspectBehavior::Panic] {
4763 let error = run_failed_test(&log_source, behavior).unwrap_err();
4764 assert_eq!(error.to_string(), "original test failure");
4765 assert_eq!(inspect_attachment_count(&log_source), 1);
4766 }
4767 }
4768
4769 #[test]
4770 fn test_vmbus_storage_controller() {
4771 let mut controller = VmbusStorageController::new(Vtl::Vtl0, VmbusStorageType::Scsi);
4772 assert_eq!(
4773 controller.set_drive(Some(1), Drive::new(None, false), false),
4774 1
4775 );
4776 assert!(controller.drives.contains_key(&1));
4777 assert_eq!(
4778 controller.set_drive(None, Drive::new(None, false), false),
4779 0
4780 );
4781 assert!(controller.drives.contains_key(&0));
4782 assert_eq!(
4783 controller.set_drive(None, Drive::new(None, false), false),
4784 2
4785 );
4786 assert!(controller.drives.contains_key(&2));
4787 assert_eq!(
4788 controller.set_drive(Some(0), Drive::new(None, false), true),
4789 0
4790 );
4791 assert!(controller.drives.contains_key(&0));
4792 }
4793}