Skip to main content

petri/vm/
mod.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4/// Hyper-V VM management
5#[cfg(windows)]
6pub mod hyperv;
7/// OpenVMM VM management
8pub mod openvmm;
9/// QEMU full machine emulation management
10pub 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
73/// The set of artifacts and resources needed to instantiate a
74/// [`PetriVmBuilder`].
75pub struct PetriVmArtifacts<T: PetriVmmBackend> {
76    /// Artifacts needed to launch the host VMM used for the test
77    pub backend: T,
78    /// Firmware and/or OS to load into the VM and associated settings
79    pub firmware: Firmware,
80    /// The architecture of the VM
81    pub arch: MachineArch,
82    /// Agent to run in the guest
83    pub agent_image: Option<AgentImage>,
84    /// Agent to run in OpenHCL
85    pub openhcl_agent_image: Option<AgentImage>,
86    /// Raw pipette binary path (for embedding in initrd via CPIO append)
87    pub pipette_binary: Option<ResolvedArtifact>,
88    /// Nested test deps
89    pub nested_test: Option<NestedTestDeps>,
90}
91
92impl<T: PetriVmmBackend> PetriVmArtifacts<T> {
93    /// Resolves the artifacts needed to instantiate a [`PetriVmBuilder`].
94    ///
95    /// Returns `None` if the supplied configuration is not supported on this platform.
96    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    /// Resolves the artifacts needed to instantiate a [`PetriVmBuilder`].
106    ///
107    /// Returns `None` if the supplied configuration is not supported on this platform.
108    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        // Cross-OS nesting needs shared artifacts because the host cannot query
116        // the nested binary. Windows CI does not yet provision those artifacts.
117        // TODO: enable cross-OS nesting on Windows once provisioning is available.
118        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
190/// Petri VM builder
191pub struct PetriVmBuilder<T: PetriVmmBackend> {
192    /// Artifacts needed to launch the host VMM used for the test
193    backend: T,
194    /// VM configuration
195    config: PetriVmConfig,
196    /// Function to modify the VMM-specific configuration
197    modify_vmm_config: Option<ModifyFn<T::VmmConfig>>,
198    /// VMM-agnostic resources
199    resources: PetriVmResources,
200
201    // VMM-specific quirks for the configured firmware
202    guest_quirks: GuestQuirksInner,
203    vmm_quirks: VmmQuirks,
204
205    // Test-specific boot behavior expectations.
206    // Defaults to expected behavior for firmware configuration.
207    expected_boot_event: Option<FirmwareEvent>,
208    override_expect_reset: bool,
209
210    // Config that is used to modify the `PetriVmConfig` before it is passed
211    // to the VMM backend.
212    /// Agent to run in the guest
213    agent_image: Option<AgentImage>,
214    /// Agent to run in OpenHCL
215    openhcl_agent_image: Option<AgentImage>,
216    /// The boot device type for the VM
217    boot_device_type: BootDeviceType,
218    /// Override for the PCIe root port used for a PCIe boot disk.
219    /// Defaults to `s0rc0rp0`.
220    pcie_boot_port: Option<String>,
221
222    // Minimal mode: skip default devices, serial, save/restore.
223    minimal_mode: bool,
224    // Raw pipette binary path (for CPIO embedding in initrd).
225    pipette_binary: Option<ResolvedArtifact>,
226    // Enable serial output even in minimal mode (for diagnostics).
227    enable_serial: bool,
228    // Enable periodic framebuffer screenshots.
229    enable_screenshots: bool,
230    // Use virtio vsock instead of VMBus-based hvsocket for guest communication.
231    use_virtio_vsock: bool,
232    // Use the Linux kernel vhost-vsock backend with this guest CID.
233    #[cfg(target_os = "linux")]
234    vhost_vsock_guest_cid: Option<u32>,
235    // Disable VMBus entirely (no vmbus server, no vmbus storage controllers).
236    no_vmbus: bool,
237    // Disable the hypervisor (HV#1) enlightenments. Implies `no_vmbus`.
238    no_hv: bool,
239    // Capture the VM's inspect output on test failure.
240    capture_inspect_on_failure: bool,
241    watchdog_timeout: Option<Duration>,
242    external_pipette: bool,
243
244    nested_test: Option<ResolvedNestedTestDeps>,
245}
246
247/// How long to wait on a single inspect before giving up on it.
248const 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/// Petri VM configuration
276#[derive(Debug)]
277pub struct PetriVmConfig {
278    /// The name of the VM
279    pub name: String,
280    /// The architecture of the VM
281    pub arch: MachineArch,
282    /// Log levels for the host VMM process.
283    pub host_log_levels: Option<OpenvmmLogConfig>,
284    /// Firmware and/or OS to load into the VM and associated settings
285    pub firmware: Firmware,
286    /// Whether to enable guest hibernation support.
287    pub hibernation_enabled: bool,
288    /// Whether to expose an IPMI KCS interface to the guest.
289    pub ipmi_enabled: bool,
290    /// The amount of memory, in bytes, to assign to the VM
291    pub memory: MemoryConfig,
292    /// The processor topology for the VM
293    pub proc_topology: ProcessorTopology,
294    /// Expose nested virtualization capabilities to the guest
295    pub nested_virt_enabled: bool,
296    /// VM guest state
297    pub vmgs: PetriVmgsResource,
298    /// TPM configuration
299    pub tpm: Option<TpmConfig>,
300    /// Storage controllers and associated disks
301    pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
302    /// PCIe NVMe drives.
303    pub pcie_nvme_drives: Vec<PcieNvmeDrive>,
304    /// PCIe virtio-blk drives.
305    pub pcie_virtio_blk_drives: Vec<PcieVirtioBlkDrive>,
306    /// Physical NVMe devices to attach
307    pub physical_nvme_devices: HashMap<Guid, PhysicalNvmeDevice>,
308    /// Path to share with the guest
309    pub guest_shares: HashMap<String, GuestShare>,
310}
311
312/// A host directory exposed through a guest filesystem device.
313#[derive(Debug, Clone)]
314pub struct GuestShare {
315    /// Directory on the host.
316    pub host_path: PathBuf,
317    /// Absolute mount point in the Linux guest.
318    pub guest_path: String,
319}
320
321/// PCIe NVMe drive configuration.
322#[derive(Debug)]
323pub struct PcieNvmeDrive {
324    /// PCIe root port name (e.g. "s0rc0rp0").
325    pub port_name: String,
326    /// NVMe namespace ID.
327    pub nsid: u32,
328    /// The drive to attach.
329    pub drive: Drive,
330}
331
332/// PCIe virtio-blk drive configuration.
333#[derive(Debug)]
334pub struct PcieVirtioBlkDrive {
335    /// PCIe root port name (e.g. "s0rc0rp0").
336    pub port_name: String,
337    /// The drive to attach.
338    pub drive: Drive,
339}
340
341/// Physical NVMe device to assign to a VM.
342/// Only used in closed-source HyperV tests
343#[derive(Debug, Clone)]
344pub struct PhysicalNvmeDevice {
345    /// The VTL to assign the physical NVMe device to.
346    pub target_vtl: Vtl,
347    /// NVMe namespace ID.
348    pub nsid: u32,
349    /// Namespace size in MiB
350    pub namespace_size_mib: u64,
351}
352
353/// Static properties about the VM for convenience during contruction and
354/// runtime of a VMM backend
355#[derive(Clone)]
356pub struct PetriVmProperties {
357    /// Whether this VM uses OpenHCL
358    pub is_openhcl: bool,
359    /// Whether this VM is isolated
360    pub is_isolated: bool,
361    /// Whether this VM uses the PCAT BIOS
362    pub is_pcat: bool,
363    /// Whether this VM boots with linux direct
364    pub is_linux_direct: bool,
365    /// Whether this VM is using pipette in VTL0
366    pub using_vtl0_pipette: bool,
367    /// Pipette is supplied and started by the guest rather than Petri.
368    pub external_pipette: bool,
369    /// Whether this VM is using VPCI
370    pub using_vpci: bool,
371    /// The OS flavor of the guest in the VM
372    pub os_flavor: OsFlavor,
373    /// Minimal mode: skip default devices, serial, save/restore
374    pub minimal_mode: bool,
375    /// Petri injects Pipette into the initrd as PID 1.
376    pub uses_pipette_as_init: bool,
377    /// Enable serial output even in minimal mode
378    pub enable_serial: bool,
379    /// Whether the VM has a CIDATA agent disk attached
380    pub has_agent_disk: bool,
381    /// Use virtio vsock instead of VMBus-based hvsocket
382    pub use_virtio_vsock: bool,
383    /// Linux kernel vhost-vsock guest CID, when that backend is enabled.
384    #[cfg(target_os = "linux")]
385    pub vhost_vsock_guest_cid: Option<u32>,
386    /// VMBus is entirely disabled
387    pub no_vmbus: bool,
388    /// The hypervisor (HV#1) enlightenments are entirely disabled
389    pub no_hv: bool,
390}
391
392/// VM configuration that can be changed after the VM is created
393pub struct PetriVmRuntimeConfig {
394    /// VTL2 settings
395    pub vtl2_settings: Option<Vtl2Settings>,
396    /// IDE controllers and associated disks
397    pub ide_controllers: Option<[[Option<Drive>; 2]; 2]>,
398    /// Storage controllers and associated disks
399    pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
400}
401
402/// Resources used by a Petri VM during contruction and runtime
403#[derive(Debug)]
404pub struct PetriVmResources {
405    /// Driver to use for async tasks during VM construction and runtime
406    pub driver: DefaultDriver,
407    /// Log source
408    pub log_source: PetriLogSource,
409    /// Pre-built initrd with pipette already injected (skips runtime injection).
410    pub prebuilt_initrd: Option<PetriInitrd>,
411}
412
413/// Trait for VMM-specific contruction and runtime resources
414#[async_trait]
415pub trait PetriVmmBackend: Debug {
416    /// VMM-specific configuration
417    type VmmConfig: 'static;
418
419    /// Runtime object
420    type VmRuntime: PetriVmRuntime;
421
422    /// Whether the backend supports VMBus.
423    const SUPPORTS_VMBUS: bool;
424
425    /// Whether the backend supports full CPU emulation
426    ///
427    /// If this is false, then tests are silently skipped if the host
428    /// architecture does not match the guest architecture.
429    const SUPPORTS_CPU_EMULATION: bool = false;
430
431    /// Whether this backend supports sharing host directories with Linux guests.
432    const SUPPORTS_FILE_SHARING: bool = false;
433
434    /// Whether this guest can use the backend's Linux 9p shares.
435    fn supports_file_sharing(firmware: &Firmware) -> bool {
436        Self::SUPPORTS_FILE_SHARING && firmware.os_flavor() == OsFlavor::Linux
437    }
438
439    /// Check whether the combination of guest firmware, guest architecture, and
440    /// internally determined host properties is supported by the backend.
441    ///
442    /// Any combinations that return false will be silently skipped. This should
443    /// not be used to skip configurations that are never valid (for example,
444    /// PCAT AARCH64 should always result in an error), and only be used if the
445    /// configuration is sometimes valid (for example, OpenVMM + OpenHCL is only
446    /// valid on Windows X64 hosts).
447    fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool;
448
449    /// Select backend specific quirks guest and vmm quirks.
450    fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks);
451
452    /// Get the default servicing flags (based on what this backend supports)
453    fn default_servicing_flags() -> OpenHclServicingFlags;
454
455    /// Create a disk for guest crash dumps, and a post-test hook to open the disk
456    /// to allow for reading the dumps.
457    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    /// Generate an rdinit script that does the necessary configuration to
465    /// launch pipette for linux direct
466    fn build_custom_init_script(pipette_path: &str) -> Option<String>;
467
468    /// Resolve any artifacts needed to use this backend
469    ///
470    /// The architecture here is that of the guest, which may or may not be the
471    /// same as the host.
472    fn new(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self;
473
474    /// Create and start VM from the generic config using the VMM backend
475    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
484// IDE is only ever offered to VTL0
485pub(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
490// SCSI luns used for both VTL0 and VTL2
491pub(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;
494/// VTL0 SCSI controller instance guid used by Petri
495pub(crate) const PETRI_SCSI_VTL0_CONTROLLER: Guid =
496    guid::guid!("27b553e8-8b39-411b-a55f-839971a7884f");
497/// VTL2 SCSI controller instance guid used by Petri
498pub(crate) const PETRI_SCSI_VTL2_CONTROLLER: Guid =
499    guid::guid!("766e96f8-2ceb-437e-afe3-a93169e48a7c");
500/// SCSI controller instance guid offered to VTL0 by VTL2
501pub(crate) const PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER: Guid =
502    guid::guid!("6c474f47-ed39-49e6-bbb9-142177a1da6e");
503
504/// The namespace ID used by Petri for the boot disk
505pub(crate) const PETRI_NVME_BOOT_NSID: u32 = 37;
506/// VTL0 NVMe controller instance guid used by Petri
507pub(crate) const PETRI_NVME_BOOT_VTL0_CONTROLLER: Guid =
508    guid::guid!("e23a04e2-90f5-4852-bc9d-e7ac691b756c");
509/// VTL2 NVMe controller instance guid used by Petri
510pub(crate) const PETRI_NVME_BOOT_VTL2_CONTROLLER: Guid =
511    guid::guid!("92bc8346-718b-449a-8751-edbf3dcd27e4");
512
513/// PCIe root port used by Petri for the agent/cidata disk (no-vmbus mode)
514pub(crate) const PETRI_PCIE_NVME_AGENT_PORT: &str = "s0rc0rp1";
515/// NVMe namespace ID used by Petri for the agent/cidata disk (no-vmbus mode)
516pub(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
526/// A constructed Petri VM
527pub 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
546/// Wrapper around the VMM backend's runtime that captures inspect state if the
547/// VM is dropped without being torn down, which is what happens when a test
548/// fails partway through.
549struct 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        // `SimpleTest::new_async` joins the test body with the task pool, so
617        // the pool keeps polling detached tasks until they complete. This
618        // finishes before the post-test hooks run.
619        self.driver
620            .spawn("petri-inspect-on-drop", async move {
621                // A panic here would propagate out of the task pool and replace
622                // the failure the test is already reporting.
623                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    /// Create a new VM configuration.
633    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    /// Create a minimal VM builder with only the bare minimum device set.
653    ///
654    /// Unlike [`new()`](Self::new), this constructor:
655    /// - Does not add default VMBus devices (shutdown IC, KVP, etc.)
656    /// - Does not add serial ports
657    /// - Does not add SCSI controllers or crash dump disks
658    /// - Does not verify save/restore on boot
659    ///
660    /// Use builder methods to opt in to specific devices. Intended for
661    /// performance tests where minimal overhead is critical.
662    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    /// Share a directory over 9p and mount it after Pipette connects.
772    ///
773    /// The backend attaches the filesystem device automatically.
774    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    /// Whether this builder is in minimal mode.
818    pub fn is_minimal(&self) -> bool {
819        self.minimal_mode
820    }
821
822    /// Set the diagnostic watchdog deadline. A standalone runner that owns its
823    /// own cancellation policy can disable the test watchdog with `None`.
824    pub fn with_watchdog_timeout(mut self, timeout: Option<Duration>) -> Self {
825        self.watchdog_timeout = timeout;
826        self
827    }
828
829    /// Use Pipette supplied and started by the guest image, without adding an
830    /// agent disk or injecting Pipette into a Linux-direct initrd.
831    ///
832    /// OpenVMM Linux-direct boot leaves the initrd's default entry point intact
833    /// unless [`Self::with_prebuilt_initrd`] supplies an explicit `rdinit`.
834    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    /// Supply a pre-built initrd with pipette already injected.
842    ///
843    /// When set, the builder skips the runtime gzip decompress/inject/
844    /// recompress cycle, using this initrd directly. Use
845    /// [`prepare_initrd`](Self::prepare_initrd) to build the initrd
846    /// ahead of time.
847    pub fn with_prebuilt_initrd(mut self, initrd: PetriInitrd) -> Self {
848        self.resources.prebuilt_initrd = Some(initrd);
849        self
850    }
851
852    /// Pre-build the modified initrd with pipette injected.
853    ///
854    /// Reads the original initrd from the firmware artifacts, injects
855    /// the pipette binary via CPIO, and writes the result to a temp file.
856    /// Returns a [`PetriInitrd`] struct that contains a `TempFile` that
857    /// must not be dropped until after the VM boots.
858    ///
859    /// Call this once before timing, then pass the path to
860    /// [`with_prebuilt_initrd`](Self::with_prebuilt_initrd) for each
861    /// iteration.
862    pub fn prepare_initrd(&self) -> anyhow::Result<PetriInitrd> {
863        self.prepare_custom_initrd(T::build_custom_init_script)
864    }
865
866    /// Prepare an initrd with a custom script
867    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    /// Enable serial port output even in minimal mode.
911    ///
912    /// Useful for diagnostics — the serial device overhead is negligible;
913    /// the cost comes from kernel console output, which is controlled via
914    /// the kernel cmdline (`quiet loglevel=0`).
915    ///
916    /// Note: this currently only affects LinuxDirect boot (kernel cmdline
917    /// and emulated serial backends). UEFI paths are unaffected.
918    pub fn with_serial_output(mut self) -> Self {
919        self.enable_serial = true;
920        self
921    }
922
923    /// Disable serial port output.
924    ///
925    /// Suppresses serial device creation, eliminating the `[uefi]` / `[openhcl]`
926    /// log lines. Useful for performance tests where serial noise is unwanted.
927    pub fn without_serial_output(mut self) -> Self {
928        self.enable_serial = false;
929        self
930    }
931
932    /// Disable periodic framebuffer screenshots.
933    ///
934    /// Suppresses the watchdog task that takes screenshots every 2 seconds,
935    /// eliminating the "No change in framebuffer" debug log lines.
936    pub fn without_screenshots(mut self) -> Self {
937        self.enable_screenshots = false;
938        self
939    }
940
941    /// Use virtio vsock instead of VMBus-based hvsocket for guest communication.
942    /// The virtio-vsock device will use PCIe, so a PCIe root topology must be
943    /// configured.
944    ///
945    /// When enabled, a virtio-vsock device is added to the VM. This device uses
946    /// the same Unix socket relay path that hvsocket would otherwise use, so
947    /// pipette will connect using this.
948    ///
949    /// For Linux direct boot, this also adjusts the kernel command line to
950    /// blacklist hv_sock instead of virtio_vsock.
951    ///
952    /// Disable it after [`Self::with_no_vmbus`] when selecting another transport.
953    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    /// Use the Linux kernel vhost-vsock backend for guest communication.
963    ///
964    /// The host connects directly to the guest's `AF_VSOCK` listener at
965    /// `guest_cid`. The OpenVMM backend automatically uses shared guest memory,
966    /// which is required by kernel vhost.
967    #[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    /// Disable VMBus entirely.
980    ///
981    /// This removes all VMBus storage controllers. For Linux guests,
982    /// virtio-vsock is used for pipette communication. For Windows guests,
983    /// the caller must also configure TCP pipette transport via
984    /// `modify_backend(|b| b.with_tcp_pipette_nic(port, mac_address))`. The
985    /// guest must boot from a non-VMBus device (e.g. PCIe NVMe).
986    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    /// Disable the hypervisor (HV#1) enlightenments.
996    ///
997    /// This also disables VMBus, since VMBus depends on the hypervisor. On
998    /// aarch64 UEFI this causes the loader to pass the generic SEC platform
999    /// type to the firmware. This mode is not supported on x86_64 UEFI.
1000    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        // When using pipette-as-init, the VTL0 agent disk is only needed
1102        // if it carries extra files (pipette itself is in the initrd).
1103        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        // When VMBus is disabled, route the agent disk through PCIe NVMe
1122        // instead of VMBus SCSI.
1123        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        // Ensure the storage controller exists (minimal mode doesn't
1136        // add controllers upfront).
1137        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    /// Whether the VTL0 agent disk will actually be added.
1303    ///
1304    /// False when using pipette-as-init with no extra files (pipette is
1305    /// in the initrd, so the CIDATA disk isn't needed).
1306    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    /// Get properties about the vm for convenience
1315    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    /// Whether Petri will inject Pipette as PID 1 init in the initrd.
1338    ///
1339    /// True for non-OpenHCL Linux direct boot when a pipette binary is
1340    /// available. Pipette is injected into the initrd via CPIO and set
1341    /// as `rdinit=/pipette`.
1342    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    /// Whether this VM is using pipette in VTL0
1349    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    /// Build and run the VM, then wait for the VM to emit the expected boot
1359    /// event (if configured). Does not configure and start pipette. Should
1360    /// only be used for testing platforms that pipette does not support.
1361    pub async fn run_without_agent(self) -> anyhow::Result<PetriVm<T>> {
1362        self.run_core().await
1363    }
1364
1365    /// Build and run the VM, then wait for the VM to emit the expected boot
1366    /// event (if configured). Launches pipette and returns a client to it.
1367    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        // Add the boot disk now to allow the test to modify the boot type
1378        // Add the agent disks now to allow the test to add custom files
1379        self = self.add_boot_disk()?.add_agent_disks()?;
1380
1381        // Auto-prepare the initrd with pipette injected if needed.
1382        // This centralizes the injection logic so backends only ever
1383        // receive a prebuilt_initrd path.
1384        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            // The panic unwinds out of the task pool, which drops the VM and
1479            // triggers the same inspect capture as any other failure.
1480            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    /// Configure the test to expect a boot failure from the VM.
1557    /// Useful for negative tests.
1558    pub fn with_expect_boot_failure(mut self) -> Self {
1559        self.expected_boot_event = Some(FirmwareEvent::BootFailed);
1560        self
1561    }
1562
1563    /// Configure the test to not expect any boot event.
1564    /// Useful for tests that do not boot a VTL0 guest.
1565    pub fn with_expect_no_boot_event(mut self) -> Self {
1566        self.expected_boot_event = None;
1567        self
1568    }
1569
1570    /// Allow the VM to reset once at the beginning of the test. Should only be
1571    /// used if you are using a special VM configuration that causes the guest
1572    /// to reboot when it usually wouldn't.
1573    pub fn with_expect_reset(mut self) -> Self {
1574        self.override_expect_reset = true;
1575        self
1576    }
1577
1578    /// Set the VM to enable secure boot and inject the templates per OS flavor.
1579    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    /// Inject Windows secure boot templates into the VM's UEFI.
1597    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    /// Inject UEFI CA secure boot templates into the VM's UEFI.
1607    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    /// Apply a custom UEFI variable delta encoded as JSON.
1617    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    /// Set the VM to use the specified processor topology.
1627    pub fn with_processor_topology(mut self, topology: ProcessorTopology) -> Self {
1628        self.config.proc_topology = topology;
1629        self
1630    }
1631
1632    /// Set the VM to use the specified memory config.
1633    pub fn with_memory(mut self, memory: MemoryConfig) -> Self {
1634        self.config.memory = memory;
1635        self
1636    }
1637
1638    /// Sets a custom OpenHCL IGVM VTL2 address type. This controls the behavior
1639    /// of where VTL2 is placed in address space, and also the total size of memory
1640    /// allocated for VTL2. VTL2 start will fail if `address_type` is specified
1641    /// and leads to the loader allocating less memory than what is in the IGVM file.
1642    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    /// Sets a custom OpenHCL IGVM file to use.
1652    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    /// Load OpenHCL from VMGS, falling back to in-box firmware (Hyper-V only).
1670    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    /// Append additional command line arguments to pass to the paravisor.
1688    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    /// Configure whether OpenHCL enables MANA keepalive at boot.
1702    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    /// Enable confidential filtering, even if the VM is not confidential.
1712    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    /// Sets the command line parameters passed to OpenHCL related to logging.
1724    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    /// Sets the log levels for the host OpenVMM process.
1734    /// DEVNOTE: In the future, this could be generalized for both HyperV and OpenVMM.
1735    /// For now, this is only implemented for OpenVMM.
1736    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    /// Adds a file to the VM's pipette agent image.
1750    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    /// Adds a file to the paravisor's pipette agent image.
1759    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    /// Sets whether UEFI frontpage is enabled.
1768    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    /// Sets the UEFI diagnostics log level filter.
1778    ///
1779    /// By default only ERROR and WARN level entries are forwarded to the
1780    /// host tracing infrastructure. Use this to also surface INFO (or all)
1781    /// entries when a test needs to observe them.
1782    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    /// Sets the per-period rate-limit override for UEFI diagnostics emission.
1792    ///
1793    /// - Not called: use the built-in defaults.
1794    /// - `0`: disable rate limiting entirely (emit every entry).
1795    /// - `n > 0`: use `n` as the per-period limit.
1796    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    /// Sets whether UEFI should always attempt a default boot.
1806    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    /// Force UEFI to bounce-buffer all DMA traffic.
1816    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    /// Run the VM with Enable VMBus relay enabled
1826    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    /// Enable guest hibernation support.
1836    ///
1837    /// Applies to any firmware type: for OpenHCL this sets the DPS
1838    /// `enable_hibernation` flag; for OpenVMM UEFI/PCAT firmware it enables the
1839    /// firmware's hibernation support.
1840    pub fn with_hibernation_enabled(mut self, enable: bool) -> Self {
1841        self.config.hibernation_enabled = enable;
1842        self
1843    }
1844
1845    /// Enable the IPMI KCS interface for an OpenHCL UEFI VM.
1846    pub fn with_ipmi(mut self, enable: bool) -> Self {
1847        self.config.ipmi_enabled = enable;
1848        self
1849    }
1850
1851    /// Specify the guest state lifetime for the VM
1852    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    /// Specify the guest state encryption policy for the VM
1879    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    /// Use the specified backing VMGS file
1894    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    /// Use the specified backing VMGS file
1899    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    /// Set the boot device type for the VM.
1921    ///
1922    /// This overrides the default, which is determined by the firmware type.
1923    pub fn with_boot_device_type(mut self, boot: BootDeviceType) -> Self {
1924        self.boot_device_type = boot;
1925        self
1926    }
1927
1928    /// Override the PCIe root port used by [`BootDeviceType::PcieNvme`] or
1929    /// [`BootDeviceType::PcieVirtioBlk`].
1930    ///
1931    /// The named port must exist in the PCIe topology added via the backend's
1932    /// `with_pcie_root_topology`. Defaults to `s0rc0rp0`.
1933    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    /// Enable the TPM for the VM.
1939    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    /// Enable or disable the TPM state persistence for the VM.
1949    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    /// Set the hardware sealing policy for the VM's TPM.
1959    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    /// Select which TPM reference implementation version the VM's TPM runs.
1969    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    /// Add custom VTL 2 settings.
1979    // TODO: At some point we want to replace uses of this with nicer with_disk,
1980    // with_nic, etc. methods.
1981    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    /// Add a storage controller to VTL2
1994    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    /// Add an additional SCSI controller to the VM.
2005    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    /// Add a VMBus disk drive to the VM
2026    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    /// Add a VMBus disk drive to the VM
2044    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    /// Add a physical NVMe device to the VM
2060    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    /// Get VM's guest OS flavor
2073    pub fn os_flavor(&self) -> OsFlavor {
2074        self.config.firmware.os_flavor()
2075    }
2076
2077    /// Get whether the VM will use OpenHCL
2078    pub fn is_openhcl(&self) -> bool {
2079        self.config.firmware.is_openhcl()
2080    }
2081
2082    /// Get the isolation type of the VM
2083    pub fn isolation(&self) -> Option<IsolationType> {
2084        self.config.firmware.isolation()
2085    }
2086
2087    /// Get the machine architecture
2088    pub fn arch(&self) -> MachineArch {
2089        self.config.arch
2090    }
2091
2092    /// Get the log source for creating additional log files.
2093    pub fn log_source(&self) -> &PetriLogSource {
2094        &self.resources.log_source
2095    }
2096
2097    /// Get the default OpenHCL servicing flags for this config
2098    pub fn default_servicing_flags(&self) -> OpenHclServicingFlags {
2099        T::default_servicing_flags()
2100    }
2101
2102    /// Apply a backend-specific modification in registration order.
2103    /// May be called multiple times, including around platform presets.
2104    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 we can just the files directly, make sure the nested test is under
2125        // the test content dir.
2126        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        // Allocate an extra 150 MB for logs and crash dumps
2191        image.allocate_free_space(150 * 1024 * 1024);
2192
2193        self
2194    }
2195
2196    /// Request nested virtualization support from the host hypervisor.
2197    pub fn with_nested_virt(mut self) -> Self {
2198        self.config.nested_virt_enabled = true;
2199        self
2200    }
2201
2202    /// Use a non-standard kernel for Linux direct tests
2203    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    /// Immediately tear down the VM.
2217    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    /// Wait for the VM to halt, returning the reason for the halt.
2224    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    /// Wait for the VM to cleanly shutdown.
2233    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    /// Wait for the VM to halt, returning the reason for the halt,
2243    /// and tear down the VM.
2244    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    /// Wait for the VM to cleanly shutdown and tear down the VM.
2251    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    /// Wait for the VM to reset. Does not wait for pipette.
2257    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    /// Wait for the VM to reset and pipette to connect.
2264    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    /// Invoke Inspect on the running OpenHCL instance.
2280    ///
2281    /// IMPORTANT: As mentioned in the Guide, inspect output is *not* guaranteed
2282    /// to be stable. Use this to test that components in OpenHCL are working as
2283    /// you would expect. But, if you are adding a test simply to verify that
2284    /// the inspect output as some other tool depends on it, then that is
2285    /// incorrect.
2286    ///
2287    /// - `timeout` is enforced on the client side
2288    /// - `path` and `depth` are passed to the [`inspect::Inspect`] machinery.
2289    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    /// Invoke Update (Inspect protocol) on the running OpenHCL instance.
2301    ///
2302    /// IMPORTANT: As mentioned in the Guide, inspect output is *not* guaranteed
2303    /// to be stable. Use this to test that components in OpenHCL are working as
2304    /// you would expect. But, if you are adding a test simply to verify that
2305    /// the inspect output as some other tool depends on it, then that is
2306    /// incorrect.
2307    ///
2308    /// - `path` and `value` are passed to the [`inspect::Inspect`] machinery.
2309    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    /// Test that we are able to inspect OpenHCL.
2320    pub async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()> {
2321        self.inspect_openhcl("", None, None).await.map(|_| ())
2322    }
2323
2324    /// Dump OpenHCL's saved state through the diagnostic interface.
2325    ///
2326    /// A running VM is briefly paused to capture its state, then resumed.
2327    pub async fn dump_openhcl_saved_state(&self) -> anyhow::Result<Vec<u8>> {
2328        self.openhcl_diag()?.dump_saved_state().await
2329    }
2330
2331    /// Invoke Inspect on the running VMM process itself (e.g. OpenVMM),
2332    /// returning the inspect tree rooted at `path` (pass `""` for the whole
2333    /// tree).
2334    ///
2335    /// Only backends that expose an inspect interface (currently OpenVMM)
2336    /// support this; other backends return an error.
2337    ///
2338    /// IMPORTANT: As mentioned in the Guide, inspect output is *not* guaranteed
2339    /// to be stable. Use this to verify that components are working as you
2340    /// expect, not to assert on output that some other tool depends on.
2341    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    /// Wait for VTL 2 to report that it is ready to respond to commands.
2352    /// Will fail if the VM is not running OpenHCL.
2353    ///
2354    /// This should only be necessary if you're doing something manual. All
2355    /// Petri-provided methods will wait for VTL 2 to be ready automatically.
2356    pub async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()> {
2357        self.openhcl_diag()?.wait_for_vtl2().await
2358    }
2359
2360    /// Get the kmsg stream from OpenHCL.
2361    pub async fn kmsg(&self) -> anyhow::Result<diag_client::kmsg_stream::KmsgStream> {
2362        self.openhcl_diag()?.kmsg().await
2363    }
2364
2365    /// Gets a live core dump of the OpenHCL process specified by 'name' and
2366    /// writes it to 'path'
2367    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    /// Crashes the specified openhcl process
2372    pub async fn openhcl_crash(&self, name: &str) -> anyhow::Result<()> {
2373        self.openhcl_diag()?.crash(name).await
2374    }
2375
2376    /// Wait for a connection from a pipette agent running in the guest.
2377    /// Useful if you've rebooted the vm or are otherwise expecting a fresh connection.
2378    async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient> {
2379        // As a workaround for #2470 (where the guest crashes when the pipette
2380        // connection timeout expires due to a vmbus bug), wait for the shutdown
2381        // IC to come online first so that we probably won't time out when
2382        // connecting to the agent.
2383        // TODO: remove this once the bug is fixed, since it shouldn't be
2384        // necessary and a guest could in theory support pipette and not the IC
2385        //
2386        // This is a no-op when the shutdown IC is not configured (e.g.,
2387        // no VMBus or minimal mode).
2388        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            // Mount parents first so they do not hide descendant shares.
2402            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    /// Wait for a connection from a pipette agent running in VTL 2.
2438    /// Useful if you've reset VTL 2 or are otherwise expecting a fresh connection.
2439    /// Will fail if the VM is not running OpenHCL.
2440    pub async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient> {
2441        // VTL 2's pipette doesn't auto launch, only launch it on demand
2442        self.launch_vtl2_pipette().await?;
2443        self.runtime.wait_for_agent(true).await
2444    }
2445
2446    /// Waits for an event emitted by the firmware about its boot status, and
2447    /// verifies that it is the expected success value.
2448    ///
2449    /// * Linux Direct guests do not emit a boot event, so this method immediately returns Ok.
2450    /// * PCAT guests may not emit an event depending on the PCAT version, this
2451    ///   method is best effort for them.
2452    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    /// Waits for an event emitted by the firmware about its boot status, and
2468    /// returns that status.
2469    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    /// Wait for the Hyper-V shutdown IC to be ready and use it to instruct
2498    /// the guest to shutdown.
2499    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        // all guests used in testing have been observed to intermittently
2504        // drop shutdown requests if they are sent too soon after the shutdown
2505        // ic comes online. give them a little extra time.
2506        // TODO: use a different method of determining whether the VM has booted
2507        // or debug and fix the shutdown IC.
2508        let mut wait_time = Duration::from_secs(10);
2509
2510        // some guests need even more time
2511        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    /// Instruct the OpenHCL to restart the VTL2 paravisor. Will fail if the VM
2528    /// is not running OpenHCL. Will also fail if the VM is not running.
2529    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    /// Update the command line parameter of the running VM that will apply on next boot.
2540    /// Will fail if the VM is not using IGVM load mode.
2541    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    /// Hot-add a PCIe device to a named port at runtime.
2546    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    /// Hot-remove a PCIe device from a named port at runtime.
2555    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    /// Instruct the OpenHCL to save the state of the VTL2 paravisor. Will fail if the VM
2560    /// is not running OpenHCL. Will also fail if the VM is not running or if this is called twice in succession
2561    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    /// Instruct the OpenHCL to restore the state of the VTL2 paravisor. Will fail if the VM
2570    /// is not running OpenHCL. Will also fail if the VM is running or if this is called without prior save
2571    pub async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
2572        self.runtime.restore_openhcl().await
2573    }
2574
2575    /// Get VM's guest OS flavor
2576    pub fn arch(&self) -> MachineArch {
2577        self.arch
2578    }
2579
2580    /// Get the inner runtime backend to make backend-specific calls
2581    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        // Start pipette through DiagClient
2589        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    /// Get the path to the VM's guest state file
2619    pub async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2620        self.runtime.get_guest_state_file().await
2621    }
2622
2623    /// Modify OpenHCL VTL2 settings.
2624    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    /// Get the list of storage controllers added to this VM
2641    pub fn get_vmbus_storage_controllers(&self) -> &HashMap<Guid, VmbusStorageController> {
2642        &self.config.vmbus_storage_controllers
2643    }
2644
2645    /// Add or modify a VMBus disk drive
2646    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    /// Run the nested test. Failed if a nested test was not configured.
2669    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        // TODO: output JSON, parse, and correctly format in logs
2725        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        // copy the files if we aren't sharing the results folder directly
2740        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/// A running VM that tests can interact with.
2772#[async_trait]
2773pub trait PetriVmRuntime: Send + Sync + 'static {
2774    /// Interface for inspecting the VM
2775    type VmInspector: PetriVmInspector;
2776    /// Interface for accessing the framebuffer
2777    type VmFramebufferAccess: PetriVmFramebufferAccess;
2778
2779    /// Cleanly tear down the VM immediately.
2780    async fn teardown(self) -> anyhow::Result<()>;
2781    /// Wait for the VM to halt, returning the reason for the halt. The VM
2782    /// should automatically restart the VM on reset if `allow_reset` is true.
2783    async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReasonDetail>;
2784    /// Wait for a connection from a pipette agent
2785    async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>;
2786    /// Get an OpenHCL diagnostics handler for the VM
2787    fn openhcl_diag(&self) -> Option<OpenHclDiagHandler>;
2788    /// Waits for an event emitted by the firmware about its boot status, and
2789    /// returns that status. Returns `None` if `timeout` elapsed first.
2790    async fn wait_for_boot_event(
2791        &mut self,
2792        timeout: Option<Duration>,
2793    ) -> anyhow::Result<Option<FirmwareEvent>>;
2794    /// Waits for the Hyper-V shutdown IC to be ready
2795    // TODO: return a receiver that will be closed when it is no longer ready.
2796    async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()>;
2797    /// Instruct the guest to shutdown via the Hyper-V shutdown IC.
2798    async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
2799    /// Instruct the OpenHCL to restart the VTL2 paravisor. Will fail if the VM
2800    /// is not running OpenHCL. Will also fail if the VM is not running.
2801    async fn restart_openhcl(
2802        &mut self,
2803        new_openhcl: &ResolvedArtifact,
2804        flags: OpenHclServicingFlags,
2805    ) -> anyhow::Result<()>;
2806    /// Instruct the OpenHCL to save the state of the VTL2 paravisor. Will fail if the VM
2807    /// is not running OpenHCL. Will also fail if the VM is not running or if this is called twice in succession
2808    /// without a call to `restore_openhcl`.
2809    async fn save_openhcl(
2810        &mut self,
2811        new_openhcl: &ResolvedArtifact,
2812        flags: OpenHclServicingFlags,
2813    ) -> anyhow::Result<()>;
2814    /// Instruct the OpenHCL to restore the state of the VTL2 paravisor. Will fail if the VM
2815    /// is not running OpenHCL. Will also fail if the VM is running or if this is called without prior save.
2816    async fn restore_openhcl(&mut self) -> anyhow::Result<()>;
2817    /// Update the command line parameter of the running VM that will apply on next boot.
2818    /// Will fail if the VM is not using IGVM load mode.
2819    async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()>;
2820    /// If the backend supports it, get an inspect interface
2821    fn inspector(&self) -> Option<Self::VmInspector> {
2822        None
2823    }
2824    /// If the backend supports it, take the screenshot interface
2825    /// (subsequent calls may return None).
2826    fn take_framebuffer_access(&mut self) -> Option<Self::VmFramebufferAccess> {
2827        None
2828    }
2829    /// Issue a hard reset to the VM
2830    async fn reset(&mut self) -> anyhow::Result<()>;
2831    /// Get the path to the VM's guest state file
2832    async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2833        Ok(None)
2834    }
2835    /// Set the OpenHCL VTL2 settings
2836    async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()>;
2837    /// Add or modify a VMBus disk drive
2838    async fn set_vmbus_drive(
2839        &mut self,
2840        disk: &Drive,
2841        controller_id: &Guid,
2842        controller_location: u32,
2843    ) -> anyhow::Result<()>;
2844    /// Hot-add a PCIe device to a named port at runtime.
2845    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    /// Hot-remove a PCIe device from a named port at runtime.
2854    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/// Interface for getting information about the state of the VM
2861#[async_trait]
2862pub trait PetriVmInspector: Send + Sync + 'static {
2863    /// Get information about the state of the VM at the given inspect `path`.
2864    /// Pass `""` to inspect the entire tree.
2865    async fn inspect(&self, path: &str) -> anyhow::Result<inspect::Node>;
2866}
2867
2868/// Use this for the associated type if not supported
2869pub 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
2877/// Raw VM screenshot
2878pub struct VmScreenshotMeta {
2879    /// color encoding used by the image
2880    pub color: image::ExtendedColorType,
2881    /// x dimension
2882    pub width: u16,
2883    /// y dimension
2884    pub height: u16,
2885}
2886
2887/// Interface for getting screenshots of the VM
2888#[async_trait]
2889pub trait PetriVmFramebufferAccess: Send + 'static {
2890    /// Populates the provided buffer with a screenshot of the VM,
2891    /// returning the dimensions and color type.
2892    async fn screenshot(&mut self, image: &mut Vec<u8>)
2893    -> anyhow::Result<Option<VmScreenshotMeta>>;
2894}
2895
2896/// Use this for the associated type if not supported
2897pub 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/// Common processor topology information for the VM.
2909#[derive(Debug)]
2910pub struct ProcessorTopology {
2911    /// The number of virtual processors.
2912    pub vp_count: u32,
2913    /// Whether SMT (hyperthreading) is enabled.
2914    pub enable_smt: Option<bool>,
2915    /// The number of virtual processors per socket.
2916    pub vps_per_socket: Option<u32>,
2917    /// The APIC configuration (x86-64 only).
2918    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    /// A large number of VPs
2934    pub fn heavy() -> Self {
2935        Self {
2936            vp_count: 16,
2937            vps_per_socket: Some(8),
2938            ..Default::default()
2939        }
2940    }
2941
2942    /// A very large number of VPs
2943    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/// The APIC mode for the VM.
2953#[derive(Debug, Clone, Copy)]
2954pub enum ApicMode {
2955    /// xAPIC mode only.
2956    Xapic,
2957    /// x2APIC mode supported but not enabled at boot.
2958    X2apicSupported,
2959    /// x2APIC mode enabled at boot.
2960    X2apicEnabled,
2961}
2962
2963/// Common memory configuration information for the VM.
2964#[derive(Debug)]
2965pub struct MemoryConfig {
2966    /// Specifies the amount of memory, in bytes, to assign to the
2967    /// virtual machine.
2968    pub startup_bytes: u64,
2969    /// Specifies the minimum and maximum amount of dynamic memory, in bytes.
2970    ///
2971    /// Dynamic memory will be disabled if this is `None`.
2972    pub dynamic_memory_range: Option<(u64, u64)>,
2973    /// Per-NUMA-node memory sizes. When set, RAM is distributed across
2974    /// vNUMA nodes instead of assigning all RAM to node 0.
2975    pub numa_mem_sizes: Option<Vec<u64>>,
2976    /// Whether to back guest RAM with private anonymous memory rather than a
2977    /// shared (file/memfd-backed) memory section.
2978    ///
2979    /// - `None` (the default) uses private memory whenever the configuration
2980    ///   allows it, falling back to shared memory otherwise. Private anonymous
2981    ///   memory is cheaper to set up and eligible for Transparent Huge Pages,
2982    ///   so it is preferred for performance; this lets each VM get the best
2983    ///   backing for its firmware without the test having to know the details.
2984    /// - `Some(true)` explicitly requires private memory. This is an error if
2985    ///   the configuration is incompatible with private memory (OpenHCL, which
2986    ///   shares VTL0 RAM with VTL2 via a remote mapper, and PCAT/Gen1, which
2987    ///   relies on x86 legacy support, both require shared memory), rather than
2988    ///   silently downgrading to shared.
2989    /// - `Some(false)` explicitly requires shared memory, for tests that need
2990    ///   the guest RAM backing to be shareable with another process, such as
2991    ///   vhost-user backends.
2992    ///
2993    /// Only applies to the OpenVMM backend; ignored by Hyper-V.
2994    pub private_memory: Option<bool>,
2995    /// Mark guest RAM as eligible for Transparent Huge Pages (THP),
2996    /// improving performance for large allocations.
2997    ///
2998    /// Defaults to `true`. Applies to private anonymous guest RAM and to
2999    /// shared memfd-backed RAM, on Linux (via `madvise`) and on Windows (via
3000    /// soft large pages). It has no effect on explicit hugetlb/large-page
3001    /// backings (see
3002    /// [`with_hugepages`](crate::openvmm::PetriVmConfigOpenVmm::with_hugepages)),
3003    /// which are already huge.
3004    ///
3005    /// Only applies to the OpenVMM backend; ignored by Hyper-V.
3006    pub transparent_hugepages: bool,
3007}
3008
3009impl Default for MemoryConfig {
3010    fn default() -> Self {
3011        Self {
3012            startup_bytes: 4 * 1024 * 1024 * 1024, // 4 GiB
3013            dynamic_memory_range: None,
3014            numa_mem_sizes: None,
3015            private_memory: None,
3016            transparent_hugepages: true,
3017        }
3018    }
3019}
3020
3021/// UEFI firmware configuration
3022#[derive(Debug)]
3023pub struct UefiConfig {
3024    /// Enable secure boot
3025    pub secure_boot_enabled: bool,
3026    /// Secure boot template
3027    pub secure_boot_template: Option<SecureBootTemplate>,
3028    /// Custom UEFI variable delta JSON
3029    pub custom_uefi_json: Option<Vec<u8>>,
3030    /// Disable the UEFI frontpage which will cause the VM to shutdown instead when unable to boot.
3031    pub disable_frontpage: bool,
3032    /// Always attempt a default boot
3033    pub default_boot_always_attempt: bool,
3034    /// Enable vPCI boot (for NVMe)
3035    pub enable_vpci_boot: bool,
3036    /// Force UEFI to bounce-buffer all DMA traffic
3037    pub force_dma_bounce: bool,
3038    /// EFI diagnostics log level filter
3039    pub efi_diagnostics_log_level: EfiDiagnosticsLogLevel,
3040    /// Per-period rate-limit override for EFI diagnostics emission.
3041    /// See [`PetriVmBuilder::with_efi_diagnostics_rate_limit()`] for more information.
3042    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/// EFI diagnostics log level filter.
3062///
3063/// Controls which UEFI diagnostics log entries are forwarded to the host
3064/// tracing infrastructure (and thus visible via kmsg / test output).
3065#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
3066pub enum EfiDiagnosticsLogLevel {
3067    /// Default log level (ERROR and WARN only).
3068    #[default]
3069    Default,
3070    /// Include INFO logs (ERROR, WARN, and INFO).
3071    Info,
3072    /// All log levels.
3073    Full,
3074}
3075
3076/// Control the logging configuration of OpenVMM/OpenHCL.
3077#[derive(Debug, Clone)]
3078pub enum OpenvmmLogConfig {
3079    /// Use the default log levels used by petri tests. This will forward
3080    /// `OPENVMM_LOG` and `OPENVMM_SHOW_SPANS` from the environment if they are
3081    /// set, otherwise it will use `debug` and `false` respectively
3082    TestDefault,
3083    /// Use the built-in default log levels of OpenHCL/OpenVMM (e.g. don't pass
3084    /// OPENVMM_LOG or OPENVMM_SHOW_SPANS)
3085    BuiltInDefault,
3086    /// Use the provided custom log levels, specified as key/value pairs. At this time,
3087    /// simply uses the already-defined environment variables (e.g.
3088    /// `OPENVMM_LOG=info,disk_nvme=debug OPENVMM_SHOW_SPANS=true`)
3089    ///
3090    /// See the Guide and source code for configuring these logs.
3091    /// - For the host VMM: see `enable_tracing` in `tracing_init.rs` for details on
3092    ///   the accepted keys and values.
3093    /// - For OpenHCL, see `init_tracing_backend` in `openhcl/src/logging/mod.rs` for details on
3094    ///   the accepted keys and values.
3095    Custom(BTreeMap<String, String>),
3096}
3097
3098/// OpenHCL configuration
3099#[derive(Debug)]
3100pub struct OpenHclConfig {
3101    /// Whether to enable VMBus redirection
3102    pub vmbus_redirect: bool,
3103    /// Whether to enable MANA keepalive at boot.
3104    pub enable_mana_keepalive: bool,
3105    /// Test-specified command-line parameters to append to the petri generated
3106    /// command line and pass to OpenHCL. VM backends should use
3107    /// [`OpenHclConfig::command_line()`] rather than reading this directly.
3108    pub custom_command_line: Option<String>,
3109    /// Command line parameters that control OpenHCL logging behavior. Separate
3110    /// from `command_line` so that petri can decide to use default log
3111    /// levels.
3112    pub log_levels: OpenvmmLogConfig,
3113    /// How to place VTL2 in address space. If `None`, the backend VMM
3114    /// will decide on default behavior.
3115    pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
3116    /// VTL2 settings
3117    pub vtl2_settings: Option<Vtl2Settings>,
3118}
3119
3120impl OpenHclConfig {
3121    /// Returns the command line to pass to OpenHCL based on these parameters. Aggregates
3122    /// the command line and log levels.
3123    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                    // Forward OPENVMM_LOG and OPENVMM_SHOW_SPANS to OpenHCL if they're set.
3134                    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                // do nothing, use whatever the built-in default is
3152            }
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/// TPM configuration
3178#[derive(Debug)]
3179pub struct TpmConfig {
3180    /// Use ephemeral TPM state (do not persist to VMGS)
3181    pub no_persistent_secrets: bool,
3182    /// Hardware sealing policy for sealed secrets
3183    pub hardware_sealing_policy: PetriHardwareSealingPolicy,
3184    /// TPM reference implementation version
3185    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/// TPM reference implementation version used by the test infrastructure.
3199#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
3200pub enum PetriTpmVersion {
3201    /// TPM reference implementation version 1.38
3202    V138,
3203    /// TPM reference implementation version 1.85
3204    #[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/// Hardware sealing policy used by the test infrastructure.
3227///
3228/// Maps to Hyper-V `Set-GuestStateEncryptionPolicy` values and
3229/// underhill's `HardwareSealingPolicy`.
3230#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
3231pub enum PetriHardwareSealingPolicy {
3232    /// No explicit policy — the backend picks its default.
3233    #[default]
3234    Default,
3235    /// Derive the hardware sealing key from measurement hash.
3236    HashPolicy,
3237    /// Derive the hardware sealing key from signer information.
3238    SignerPolicy,
3239}
3240
3241/// Source for the IGVM firmware used to load OpenHCL.
3242#[derive(Debug)]
3243pub enum IgvmFirmwareSource {
3244    /// Load the IGVM from the specified file.
3245    File(ResolvedArtifact),
3246    /// Load the IGVM from VMGS, falling back to the in-box firmware.
3247    VmgsOrInBox,
3248}
3249
3250/// Firmware to load into the test VM.
3251// TODO: remove the guests from the firmware enum so that we don't pass them
3252// to the VMM backend after we have already used them generically.
3253#[derive(Debug)]
3254pub enum Firmware {
3255    /// Boot Linux directly, without any firmware.
3256    LinuxDirect {
3257        /// The kernel to boot.
3258        kernel: ResolvedArtifact,
3259        /// The initrd to use.
3260        initrd: ResolvedArtifact,
3261    },
3262    /// Boot an isolated Linux guest from an IGVM file with no paravisor.
3263    IgvmLinuxDirect {
3264        /// The IGVM file with the measured kernel, initrd, and bootshim.
3265        igvm_path: ResolvedArtifact,
3266        /// The isolation type of the guest.
3267        isolation: IsolationType,
3268    },
3269    /// Boot Linux directly, without any firmware, with OpenHCL in VTL2.
3270    OpenhclLinuxDirect {
3271        /// The source for the IGVM firmware.
3272        igvm_firmware: IgvmFirmwareSource,
3273        /// OpenHCL configuration
3274        openhcl_config: OpenHclConfig,
3275    },
3276    /// Boot a PCAT-based VM.
3277    Pcat {
3278        /// The guest OS the VM will boot into.
3279        guest: PcatGuest,
3280        /// The firmware to use.
3281        bios_firmware: ResolvedOptionalArtifact,
3282        /// The SVGA firmware to use.
3283        svga_firmware: ResolvedOptionalArtifact,
3284        /// IDE controllers and associated disks
3285        ide_controllers: [[Option<Drive>; 2]; 2],
3286    },
3287    /// Boot a PCAT-based VM with OpenHCL in VTL2.
3288    OpenhclPcat {
3289        /// The guest OS the VM will boot into.
3290        guest: PcatGuest,
3291        /// The source for the IGVM firmware.
3292        igvm_firmware: IgvmFirmwareSource,
3293        /// The firmware to use.
3294        bios_firmware: ResolvedOptionalArtifact,
3295        /// The SVGA firmware to use.
3296        svga_firmware: ResolvedOptionalArtifact,
3297        /// OpenHCL configuration
3298        openhcl_config: OpenHclConfig,
3299    },
3300    /// Boot a UEFI-based VM.
3301    Uefi {
3302        /// The guest OS the VM will boot into.
3303        guest: UefiGuest,
3304        /// The firmware to use.
3305        uefi_firmware: ResolvedArtifact,
3306        /// UEFI configuration
3307        uefi_config: UefiConfig,
3308    },
3309    /// Boot a UEFI-based VM with OpenHCL in VTL2.
3310    OpenhclUefi {
3311        /// The guest OS the VM will boot into.
3312        guest: UefiGuest,
3313        /// The isolation type of the VM.
3314        isolation: Option<IsolationType>,
3315        /// The source for the IGVM firmware.
3316        igvm_firmware: IgvmFirmwareSource,
3317        /// UEFI configuration
3318        uefi_config: UefiConfig,
3319        /// OpenHCL configuration
3320        openhcl_config: OpenHclConfig,
3321    },
3322}
3323
3324/// The boot device type.
3325#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3326pub enum BootDeviceType {
3327    /// Don't initialize a boot device.
3328    None,
3329    /// Boot from IDE.
3330    Ide,
3331    /// Boot from IDE via SCSI to VTL2.
3332    IdeViaScsi,
3333    /// Boot from IDE via NVME to VTL2.
3334    IdeViaNvme,
3335    /// Boot from SCSI.
3336    Scsi,
3337    /// Boot from SCSI via SCSI to VTL2.
3338    ScsiViaScsi,
3339    /// Boot from SCSI via NVME to VTL2.
3340    ScsiViaNvme,
3341    /// Boot from NVMe.
3342    Nvme,
3343    /// Boot from NVMe via SCSI to VTL2.
3344    NvmeViaScsi,
3345    /// Boot from NVMe via NVMe to VTL2.
3346    NvmeViaNvme,
3347    /// Boot from NVMe attached to a PCIe root port.
3348    PcieNvme,
3349    /// Boot from virtio-blk attached to a PCIe root port.
3350    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    /// Constructs a standard [`Firmware::LinuxDirect`] configuration, or a
3398    /// [`Firmware::IgvmLinuxDirect`] configuration if `isolation` is set.
3399    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    /// Constructs a [`Firmware::LinuxDirect`] configuration that uses a
3426    /// compressed bzImage kernel instead of an uncompressed ELF.
3427    ///
3428    /// This is x86_64-only, as bzImage is an x86-specific format.
3429    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    /// Constructs a standard [`Firmware::OpenhclLinuxDirect`] configuration.
3438    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    /// Constructs a standard [`Firmware::Pcat`] configuration.
3452    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    /// Constructs a standard [`Firmware::OpenhclPcat`] configuration.
3463    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 is necessary for IDE to be provided by VTL2
3473                vmbus_redirect: true,
3474                ..Default::default()
3475            },
3476        }
3477    }
3478
3479    /// Constructs a standard [`Firmware::Uefi`] configuration.
3480    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    /// Constructs a standard [`Firmware::OpenhclUefi`] configuration.
3494    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    /// Isolated guest with no paravisor
3540    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    /// Get the initrd path for Linux direct boot firmware.
3557    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                // TODO: Handle older PCAT versions that don't fire the event
3660                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/// The guest the VM will boot into. A boot drive with the chosen setup
3825/// will be automatically configured.
3826#[derive(Debug)]
3827pub enum PcatGuest {
3828    /// Mount a VHD as the boot drive.
3829    Vhd(BootImageConfig<boot_image_type::Vhd>),
3830    /// Mount an ISO as the CD/DVD drive.
3831    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/// The guest the VM will boot into. A boot drive with the chosen setup
3848/// will be automatically configured.
3849#[derive(Debug)]
3850pub enum UefiGuest {
3851    /// Compose an mshv root-partition boot disk from resolved artifacts and a
3852    /// prepared initrd. Requires x86-64 UEFI without OpenHCL.
3853    Mshv(MshvBoot),
3854    /// Mount a VHD as the boot drive.
3855    Vhd(BootImageConfig<boot_image_type::Vhd>),
3856    /// The UEFI test image produced by our guest-test infrastructure.
3857    GuestTestUefi(ResolvedArtifact),
3858    /// No guest, just the firmware.
3859    None,
3860}
3861
3862impl UefiGuest {
3863    /// Construct a standard [`UefiGuest::GuestTestUefi`] configuration.
3864    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
3883/// Type-tags for [`BootImageConfig`](super::BootImageConfig)
3884pub 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    /// Private trait use to seal the set of artifact types BootImageType
3892    /// supports.
3893    pub trait BootImageType: private::Sealed {}
3894
3895    /// BootImageConfig for a VHD file
3896    #[derive(Debug)]
3897    pub enum Vhd {}
3898
3899    /// BootImageConfig for an ISO file
3900    #[derive(Debug)]
3901    pub enum Iso {}
3902
3903    impl BootImageType for Vhd {}
3904    impl BootImageType for Iso {}
3905}
3906
3907/// Configuration information for the boot drive of the VM.
3908#[derive(Debug)]
3909pub struct BootImageConfig<T: boot_image_type::BootImageType> {
3910    /// Artifact source corresponding to the boot media (local or remote).
3911    artifact: ResolvedArtifactSource,
3912    /// The OS flavor.
3913    os_flavor: OsFlavor,
3914    /// Any quirks needed to boot the guest.
3915    ///
3916    /// Most guests should not need any quirks, and can use `Default`.
3917    quirks: GuestQuirks,
3918    /// Marker denoting what type of media `artifact` corresponds to
3919    _type: core::marker::PhantomData<T>,
3920}
3921
3922impl<T: boot_image_type::BootImageType> BootImageConfig<T> {
3923    /// Get a [`DiskPath`] from the artifact source.
3924    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    /// Create a new BootImageConfig from a VHD artifact source
3934    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    /// Create a new BootImageConfig from an ISO artifact source
3949    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/// Isolation type
3963#[derive(Debug, Clone, Copy)]
3964pub enum IsolationType {
3965    /// VBS
3966    Vbs,
3967    /// SNP
3968    Snp,
3969    /// TDX
3970    Tdx,
3971}
3972
3973/// Flags controlling servicing behavior.
3974#[derive(Debug, Clone, Copy)]
3975pub struct OpenHclServicingFlags {
3976    /// Preserve DMA memory for NVMe devices if supported.
3977    /// Defaults to `true`.
3978    pub enable_nvme_keepalive: bool,
3979    /// Preserve DMA memory for MANA devices if supported.
3980    pub enable_mana_keepalive: bool,
3981    /// Skip any logic that the vmm may have to ignore servicing updates if the supplied igvm file version is not different than the one currently running.
3982    pub override_version_checks: bool,
3983    /// Hint to the OpenHCL runtime how much time to wait when stopping / saving the OpenHCL.
3984    pub stop_timeout_hint_secs: Option<u16>,
3985}
3986
3987/// Where a disk image is located.
3988#[derive(Debug, Clone)]
3989pub enum DiskPath {
3990    /// A local file path.
3991    Local(PathBuf),
3992    /// A remote URL (fetched on demand via HTTP Range requests).
3993    Remote {
3994        /// The URL where the disk can be fetched.
3995        url: String,
3996    },
3997}
3998
3999impl From<PathBuf> for DiskPath {
4000    fn from(path: PathBuf) -> Self {
4001        DiskPath::Local(path)
4002    }
4003}
4004
4005/// Petri disk
4006#[derive(Debug, Clone)]
4007pub enum Disk {
4008    /// Memory backed with specified size
4009    Memory(u64),
4010    /// Memory differencing disk backed by a VHD (local or remote)
4011    Differencing(DiskPath),
4012    /// Persistent VHD
4013    Persistent(PathBuf),
4014    /// Disk backed by a temporary VHD
4015    Temporary(Arc<TempPath>),
4016}
4017
4018/// A path that can be either temporary or persistent
4019#[derive(Debug, Clone)]
4020pub enum TempOrPersistentPath {
4021    /// Temporary path
4022    Temp(Arc<TempPath>),
4023    /// Persistent path
4024    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/// Petri initrd
4037#[derive(Debug, Clone)]
4038pub struct PetriInitrd {
4039    /// Where the initrd is located on the host system.
4040    pub path: TempOrPersistentPath,
4041    /// Path to use with `rdinit=` kernel command line parameter.
4042    pub rdinit_param: String,
4043}
4044
4045/// Petri VMGS disk
4046#[derive(Debug, Clone)]
4047pub struct PetriVmgsDisk {
4048    /// Backing disk
4049    pub disk: Disk,
4050    /// Guest state encryption policy
4051    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            // TODO: make this strict once we can set it in OpenHCL on Hyper-V
4059            encryption_policy: GuestStateEncryptionPolicy::None(false),
4060        }
4061    }
4062}
4063
4064/// Petri VM guest state resource
4065#[derive(Debug, Clone)]
4066pub enum PetriVmgsResource {
4067    /// Use disk to store guest state
4068    Disk(PetriVmgsDisk),
4069    /// Use disk to store guest state, reformatting if corrupted.
4070    ReprovisionOnFailure(PetriVmgsDisk),
4071    /// Format and use disk to store guest state
4072    Reprovision(PetriVmgsDisk),
4073    /// Store guest state in memory
4074    Ephemeral,
4075}
4076
4077impl PetriVmgsResource {
4078    /// get the inner vmgs disk if one exists
4079    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    /// get the inner disk if one exists
4089    pub fn disk(&self) -> Option<&Disk> {
4090        self.vmgs().map(|vmgs| &vmgs.disk)
4091    }
4092
4093    /// get the encryption policy of the vmgs
4094    pub fn encryption_policy(&self) -> Option<GuestStateEncryptionPolicy> {
4095        self.vmgs().map(|vmgs| vmgs.encryption_policy)
4096    }
4097}
4098
4099/// Petri VM guest state lifetime
4100#[derive(Debug, Clone, Copy)]
4101pub enum PetriGuestStateLifetime {
4102    /// Use a differencing disk backed by a blank, tempory VMGS file
4103    /// or other artifact if one is provided
4104    Disk,
4105    /// Same as default, except reformat the backing disk if corrupted
4106    ReprovisionOnFailure,
4107    /// Same as default, except reformat the backing disk
4108    Reprovision,
4109    /// Store guest state in memory (no backing disk)
4110    Ephemeral,
4111}
4112
4113/// UEFI secure boot template
4114#[derive(Debug, Clone, Copy)]
4115pub enum SecureBootTemplate {
4116    /// The Microsoft Windows template.
4117    MicrosoftWindows,
4118    /// The Microsoft UEFI certificate authority template.
4119    MicrosoftUefiCertificateAuthority,
4120}
4121
4122/// Quirks to workaround certain bugs that only manifest when using a
4123/// particular VMM, and do not depend on which guest is running.
4124#[derive(Default, Debug, Clone)]
4125pub struct VmmQuirks {
4126    /// Automatically reset the VM if we did not recieve a boot event in the
4127    /// specified amount of time.
4128    pub flaky_boot: Option<Duration>,
4129}
4130
4131/// The reason that the VM halted
4132#[derive(Debug, Clone, Copy, Eq, PartialEq)]
4133pub enum PetriHaltReason {
4134    /// The vm powered off
4135    PowerOff,
4136    /// The vm reset
4137    Reset,
4138    /// The vm hibernated
4139    Hibernate,
4140    /// The vm triple faulted
4141    TripleFault,
4142    /// The vm halted for some other reason
4143    Other,
4144}
4145
4146impl PetriHaltReason {
4147    /// Construct a halt reason with detailed debug info
4148    pub fn with_detail(self, detail: String) -> PetriHaltReasonDetail {
4149        PetriHaltReasonDetail {
4150            reason: self,
4151            detail,
4152        }
4153    }
4154}
4155
4156/// The reason that the VM halted, with optional addition debug details
4157#[derive(Debug, Clone)]
4158pub struct PetriHaltReasonDetail {
4159    /// The reason for the halt
4160    pub reason: PetriHaltReason,
4161    /// More details about the halt
4162    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
4204/// Wrapper for modification functions with stubbed out debug impl
4205pub 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
4213/// Default VTL 2 settings used by petri
4214fn 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/// Virtual trust level
4224#[derive(Debug, Copy, Clone, PartialEq, Eq)]
4225pub enum Vtl {
4226    /// VTL 0
4227    Vtl0 = 0,
4228    /// VTL 1
4229    Vtl1 = 1,
4230    /// VTL 2
4231    Vtl2 = 2,
4232}
4233
4234/// The VMBus storage device type.
4235#[derive(Debug, Copy, Clone, PartialEq, Eq)]
4236pub enum VmbusStorageType {
4237    /// SCSI
4238    Scsi,
4239    /// NVMe
4240    Nvme,
4241    /// Virtio block device
4242    VirtioBlk,
4243}
4244
4245/// VM disk drive
4246#[derive(Debug, Clone)]
4247pub struct Drive {
4248    /// Backing disk
4249    pub disk: Option<Disk>,
4250    /// Whether this is a DVD
4251    pub is_dvd: bool,
4252}
4253
4254impl Drive {
4255    /// Create a new disk
4256    pub fn new(disk: Option<Disk>, is_dvd: bool) -> Self {
4257        Self { disk, is_dvd }
4258    }
4259}
4260
4261/// VMBus storage controller
4262#[derive(Debug, Clone)]
4263pub struct VmbusStorageController {
4264    /// The VTL to assign the storage controller to
4265    pub target_vtl: Vtl,
4266    /// The storage device type
4267    pub controller_type: VmbusStorageType,
4268    /// Drives (with any inserted disks) attached to this storage controller
4269    pub drives: HashMap<u32, Drive>,
4270}
4271
4272impl VmbusStorageController {
4273    /// Create a new storage controller
4274    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    /// Add a disk to the storage controller
4283    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            // find the first available lun
4291            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
4309/// Returns the cache directory for lazy-fetched disk artifacts.
4310pub(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    // Linux / fallback: XDG
4330    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
4340/// All the information needed to locate and run a particular Nextest test
4341pub struct NestedTestDeps {
4342    /// Nextest archive artifact
4343    pub archive: ErasedArtifactHandle,
4344    /// Binary name within the nextest artifact
4345    pub binary: String,
4346    /// Test name within the binary
4347    pub test_name: String,
4348}
4349
4350impl NestedTestDeps {
4351    /// Create a new set of nested test dep arguments
4352    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
4365/// All the information needed to locate and run a particular Nextest test
4366pub struct ResolvedNestedTestDeps {
4367    /// Extracted test binary
4368    pub test_binary: TempPath,
4369    /// Test name within the binary
4370    pub test_name: String,
4371    /// Artifacts required by the test
4372    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    /// Extract the binary from the archive by parsing the json metadata
4404    fn extract_binary(&self) -> anyhow::Result<TempPath> {
4405        let temp_dir = tempfile::tempdir()?;
4406
4407        // We need to resolve this manually since we need to get the nested test
4408        // artifacts and to do that we need the archive first.
4409        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        // copy the binary under the test content dir so it can be shared with the guest
4443        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    /// Get the OS flavor
4488    fn os_flavor(&self) -> OsFlavor;
4489
4490    /// Get the path separator
4491    fn path_separator(&self) -> &'static str {
4492        match self.os_flavor() {
4493            OsFlavor::Windows => "\\",
4494            _ => "/",
4495        }
4496    }
4497
4498    /// Get the path to the agent disk, if it exists
4499    ///
4500    /// Does not include any subsequent path separators.
4501    fn agent_disk_root(&self) -> &'static str {
4502        match self.os_flavor() {
4503            OsFlavor::Windows => "D:",
4504            _ => "/cidata",
4505        }
4506    }
4507
4508    /// Construct a path with the appropriate path component separators for
4509    /// the guest.
4510    fn build_guest_path(&self, components: &[String]) -> String {
4511        components.join(self.path_separator())
4512    }
4513
4514    /// Construct a path with the appropriate path component separators for
4515    /// the guest from an existing host path.
4516    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    /// Returns the paths to each of the parent directories.
4526    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    /// Construct the path to a file in the agent disk.
4533    ///
4534    /// Panics if no agent disk exists.
4535    fn agent_disk_path(&self, components: &[String]) -> String {
4536        self.build_guest_path(&[&[self.agent_disk_root().into()], components].concat())
4537    }
4538
4539    /// Construct a binary name with the appropriate extension for the guest.
4540    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}