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    use_virtio_console: bool,
229    // Enable periodic framebuffer screenshots.
230    enable_screenshots: bool,
231    // Use virtio vsock instead of VMBus-based hvsocket for guest communication.
232    use_virtio_vsock: bool,
233    // Use the Linux kernel vhost-vsock backend with this guest CID.
234    #[cfg(target_os = "linux")]
235    vhost_vsock_guest_cid: Option<u32>,
236    // Disable VMBus entirely (no vmbus server, no vmbus storage controllers).
237    no_vmbus: bool,
238    // Disable the hypervisor (HV#1) enlightenments. Implies `no_vmbus`.
239    no_hv: bool,
240    // Capture the VM's inspect output on test failure.
241    capture_inspect_on_failure: bool,
242    watchdog_timeout: Option<Duration>,
243    external_pipette: bool,
244
245    nested_test: Option<ResolvedNestedTestDeps>,
246}
247
248/// How long to wait on a single inspect before giving up on it.
249const INSPECT_TIMEOUT: Duration = Duration::from_secs(10);
250
251impl<T: PetriVmmBackend> Debug for PetriVmBuilder<T> {
252    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253        f.debug_struct("PetriVmBuilder")
254            .field("backend", &self.backend)
255            .field("config", &self.config)
256            .field("modify_vmm_config", &self.modify_vmm_config.is_some())
257            .field("resources", &self.resources)
258            .field("guest_quirks", &self.guest_quirks)
259            .field("vmm_quirks", &self.vmm_quirks)
260            .field("expected_boot_event", &self.expected_boot_event)
261            .field("override_expect_reset", &self.override_expect_reset)
262            .field("agent_image", &self.agent_image)
263            .field("openhcl_agent_image", &self.openhcl_agent_image)
264            .field("boot_device_type", &self.boot_device_type)
265            .field("pcie_boot_port", &self.pcie_boot_port)
266            .field("minimal_mode", &self.minimal_mode)
267            .field("enable_serial", &self.enable_serial)
268            .field("use_virtio_console", &self.use_virtio_console)
269            .field("enable_screenshots", &self.enable_screenshots)
270            .field("use_virtio_vsock", &self.use_virtio_vsock)
271            .field("no_vmbus", &self.no_vmbus)
272            .field("no_hv", &self.no_hv)
273            .finish()
274    }
275}
276
277/// Petri VM configuration
278#[derive(Debug, Clone)]
279pub struct PetriVmConfig {
280    /// The name of the VM
281    pub name: String,
282    /// The architecture of the VM
283    pub arch: MachineArch,
284    /// Log levels for the host VMM process.
285    pub host_log_levels: Option<OpenvmmLogConfig>,
286    /// Firmware and/or OS to load into the VM and associated settings
287    pub firmware: Firmware,
288    /// Whether to enable guest hibernation support.
289    pub hibernation_enabled: bool,
290    /// Whether to expose an IPMI KCS interface to the guest.
291    pub ipmi_enabled: bool,
292    /// The amount of memory, in bytes, to assign to the VM
293    pub memory: MemoryConfig,
294    /// The processor topology for the VM
295    pub proc_topology: ProcessorTopology,
296    /// Expose nested virtualization capabilities to the guest
297    pub nested_virt_enabled: bool,
298    /// VM guest state
299    pub vmgs: PetriVmgsResource,
300    /// TPM configuration
301    pub tpm: Option<TpmConfig>,
302    /// Storage controllers and associated disks
303    pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
304    /// PCIe NVMe drives.
305    pub pcie_nvme_drives: Vec<PcieNvmeDrive>,
306    /// PCIe virtio-blk drives.
307    pub pcie_virtio_blk_drives: Vec<PcieVirtioBlkDrive>,
308    /// Physical NVMe devices to attach
309    pub physical_nvme_devices: HashMap<Guid, PhysicalNvmeDevice>,
310    /// Path to share with the guest
311    pub guest_shares: HashMap<String, GuestShare>,
312}
313
314/// A host directory exposed through a guest filesystem device.
315#[derive(Debug, Clone)]
316pub struct GuestShare {
317    /// Directory on the host.
318    pub host_path: PathBuf,
319    /// Absolute mount point in the Linux guest.
320    pub guest_path: String,
321}
322
323/// PCIe NVMe drive configuration.
324#[derive(Debug, Clone)]
325pub struct PcieNvmeDrive {
326    /// PCIe root port name (e.g. "s0rc0rp0").
327    pub port_name: String,
328    /// NVMe namespace ID.
329    pub nsid: u32,
330    /// The drive to attach.
331    pub drive: Drive,
332}
333
334/// PCIe virtio-blk drive configuration.
335#[derive(Debug, Clone)]
336pub struct PcieVirtioBlkDrive {
337    /// PCIe root port name (e.g. "s0rc0rp0").
338    pub port_name: String,
339    /// The drive to attach.
340    pub drive: Drive,
341}
342
343/// Physical NVMe device to assign to a VM.
344/// Only used in closed-source HyperV tests
345#[derive(Debug, Clone)]
346pub struct PhysicalNvmeDevice {
347    /// The VTL to assign the physical NVMe device to.
348    pub target_vtl: Vtl,
349    /// NVMe namespace ID.
350    pub nsid: u32,
351    /// Namespace size in MiB
352    pub namespace_size_mib: u64,
353}
354
355/// Static properties about the VM for convenience during contruction and
356/// runtime of a VMM backend
357#[derive(Clone)]
358pub struct PetriVmProperties {
359    /// Whether this VM uses OpenHCL
360    pub is_openhcl: bool,
361    /// Whether this VM is isolated
362    pub is_isolated: bool,
363    /// Whether this VM uses the PCAT BIOS
364    pub is_pcat: bool,
365    /// Whether this VM boots with linux direct
366    pub is_linux_direct: bool,
367    /// Whether this VM is using pipette in VTL0
368    pub using_vtl0_pipette: bool,
369    /// Pipette is supplied and started by the guest rather than Petri.
370    pub external_pipette: bool,
371    /// Whether this VM is using VPCI
372    pub using_vpci: bool,
373    /// The OS flavor of the guest in the VM
374    pub os_flavor: OsFlavor,
375    /// Minimal mode: skip default devices, serial, save/restore
376    pub minimal_mode: bool,
377    /// Petri injects Pipette into the initrd as PID 1.
378    pub uses_pipette_as_init: bool,
379    /// Enable serial output even in minimal mode
380    pub enable_serial: bool,
381    /// Capture a Linux virtio console independently of the firmware UART.
382    pub use_virtio_console: bool,
383    /// Whether the VM has a CIDATA agent disk attached
384    pub has_agent_disk: bool,
385    /// Use virtio vsock instead of VMBus-based hvsocket
386    pub use_virtio_vsock: bool,
387    /// Linux kernel vhost-vsock guest CID, when that backend is enabled.
388    #[cfg(target_os = "linux")]
389    pub vhost_vsock_guest_cid: Option<u32>,
390    /// VMBus is entirely disabled
391    pub no_vmbus: bool,
392    /// The hypervisor (HV#1) enlightenments are entirely disabled
393    pub no_hv: bool,
394}
395
396/// VM configuration that can be changed after the VM is created
397pub struct PetriVmRuntimeConfig {
398    /// VTL2 settings
399    pub vtl2_settings: Option<Vtl2Settings>,
400    /// IDE controllers and associated disks
401    pub ide_controllers: Option<[[Option<Drive>; 2]; 2]>,
402    /// Storage controllers and associated disks
403    pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
404}
405
406/// Resources used by a Petri VM during contruction and runtime
407#[derive(Debug)]
408pub struct PetriVmResources {
409    /// Driver to use for async tasks during VM construction and runtime
410    pub driver: DefaultDriver,
411    /// Log source
412    pub log_source: PetriLogSource,
413    /// Pre-built initrd with pipette already injected (skips runtime injection).
414    pub prebuilt_initrd: Option<PetriInitrd>,
415}
416
417/// Trait for VMM-specific contruction and runtime resources
418#[async_trait]
419pub trait PetriVmmBackend: Debug {
420    /// VMM-specific configuration
421    type VmmConfig: 'static;
422
423    /// Runtime object
424    type VmRuntime: PetriVmRuntime;
425
426    /// Whether the backend supports VMBus.
427    const SUPPORTS_VMBUS: bool;
428
429    /// Whether the backend supports full CPU emulation
430    ///
431    /// If this is false, then tests are silently skipped if the host
432    /// architecture does not match the guest architecture.
433    const SUPPORTS_CPU_EMULATION: bool = false;
434
435    /// Whether this backend supports sharing host directories with Linux guests.
436    const SUPPORTS_FILE_SHARING: bool = false;
437
438    /// Whether this guest can use the backend's Linux 9p shares.
439    fn supports_file_sharing(firmware: &Firmware) -> bool {
440        Self::SUPPORTS_FILE_SHARING && firmware.os_flavor() == OsFlavor::Linux
441    }
442
443    /// Check whether the combination of guest firmware, guest architecture, and
444    /// internally determined host properties is supported by the backend.
445    ///
446    /// Any combinations that return false will be silently skipped. This should
447    /// not be used to skip configurations that are never valid (for example,
448    /// PCAT AARCH64 should always result in an error), and only be used if the
449    /// configuration is sometimes valid (for example, OpenVMM + OpenHCL is only
450    /// valid on Windows X64 hosts).
451    fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool;
452
453    /// Select backend specific quirks guest and vmm quirks.
454    fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks);
455
456    /// Get the default servicing flags (based on what this backend supports)
457    fn default_servicing_flags() -> OpenHclServicingFlags;
458
459    /// Create a disk for guest crash dumps, and a post-test hook to open the disk
460    /// to allow for reading the dumps.
461    fn create_guest_dump_disk() -> anyhow::Result<
462        Option<(
463            Arc<TempPath>,
464            Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
465        )>,
466    >;
467
468    /// Generate an rdinit script that does the necessary configuration to
469    /// launch pipette for linux direct
470    fn build_custom_init_script(pipette_path: &str) -> Option<String>;
471
472    /// Resolve any artifacts needed to use this backend
473    ///
474    /// The architecture here is that of the guest, which may or may not be the
475    /// same as the host.
476    fn new(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self;
477
478    /// Create and start VM from the generic config using the VMM backend
479    async fn run(
480        self,
481        config: PetriVmConfig,
482        modify_vmm_config: Option<ModifyFn<Self::VmmConfig>>,
483        resources: &PetriVmResources,
484        properties: &PetriVmProperties,
485    ) -> anyhow::Result<(Self::VmRuntime, PetriVmRuntimeConfig)>;
486}
487
488// IDE is only ever offered to VTL0
489pub(crate) const PETRI_IDE_BOOT_CONTROLLER_NUMBER: u32 = 0;
490pub(crate) const PETRI_IDE_BOOT_LUN: u8 = 0;
491pub(crate) const PETRI_IDE_BOOT_CONTROLLER: Guid =
492    guid::guid!("ca56751f-e643-4bef-bf54-f73678e8b7b5");
493
494// SCSI luns used for both VTL0 and VTL2
495pub(crate) const PETRI_SCSI_BOOT_LUN: u32 = 0;
496pub(crate) const PETRI_SCSI_PIPETTE_LUN: u32 = 1;
497pub(crate) const PETRI_SCSI_CRASH_LUN: u32 = 2;
498/// VTL0 SCSI controller instance guid used by Petri
499pub(crate) const PETRI_SCSI_VTL0_CONTROLLER: Guid =
500    guid::guid!("27b553e8-8b39-411b-a55f-839971a7884f");
501/// VTL2 SCSI controller instance guid used by Petri
502pub(crate) const PETRI_SCSI_VTL2_CONTROLLER: Guid =
503    guid::guid!("766e96f8-2ceb-437e-afe3-a93169e48a7c");
504/// SCSI controller instance guid offered to VTL0 by VTL2
505pub(crate) const PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER: Guid =
506    guid::guid!("6c474f47-ed39-49e6-bbb9-142177a1da6e");
507
508/// The namespace ID used by Petri for the boot disk
509pub(crate) const PETRI_NVME_BOOT_NSID: u32 = 37;
510/// VTL0 NVMe controller instance guid used by Petri
511pub(crate) const PETRI_NVME_BOOT_VTL0_CONTROLLER: Guid =
512    guid::guid!("e23a04e2-90f5-4852-bc9d-e7ac691b756c");
513/// VTL2 NVMe controller instance guid used by Petri
514pub(crate) const PETRI_NVME_BOOT_VTL2_CONTROLLER: Guid =
515    guid::guid!("92bc8346-718b-449a-8751-edbf3dcd27e4");
516
517/// PCIe root port used by Petri for the agent/cidata disk (no-vmbus mode)
518pub(crate) const PETRI_PCIE_NVME_AGENT_PORT: &str = "s0rc0rp1";
519/// NVMe namespace ID used by Petri for the agent/cidata disk (no-vmbus mode)
520pub(crate) const PETRI_PCIE_NVME_AGENT_NSID: u32 = 1;
521
522const NESTED_TEST_CONTENT_DIR: &str = "vmm_tests";
523const NESTED_IMAGES_DIR: &str = "images";
524const NESTED_RESULTS_DIR: &str = "test_results";
525const NESTED_TEST_BINARY: &str = "nested_test";
526const NESTED_TEMP_DIR: &str = "temp";
527const NESTED_CONTENT_TAG: &str = "testcontent";
528const NESTED_IMAGES_TAG: &str = "testimages";
529
530/// A constructed Petri VM
531pub struct PetriVm<T: PetriVmmBackend> {
532    resources: PetriVmResources,
533    runtime: PetriVmRuntimeGuard<T::VmRuntime>,
534    watchdog_tasks: Vec<Task<()>>,
535    openhcl_diag_handler: Option<OpenHclDiagHandler>,
536
537    arch: MachineArch,
538    guest_quirks: GuestQuirksInner,
539    vmm_quirks: VmmQuirks,
540    expected_boot_event: Option<FirmwareEvent>,
541
542    config: PetriVmRuntimeConfig,
543    properties: PetriVmProperties,
544
545    nested_test: Option<ResolvedNestedTestDeps>,
546    guest_shares: HashMap<String, GuestShare>,
547    name: String,
548}
549
550/// Wrapper around the VMM backend's runtime that captures inspect state if the
551/// VM is dropped without being torn down, which is what happens when a test
552/// fails partway through.
553struct PetriVmRuntimeGuard<T: PetriVmRuntime> {
554    runtime: Option<T>,
555    driver: DefaultDriver,
556    log_source: PetriLogSource,
557    capture_inspect_on_drop: bool,
558}
559
560impl<T: PetriVmRuntime> PetriVmRuntimeGuard<T> {
561    fn take_for_teardown(&mut self) -> T {
562        self.runtime.take().expect("runtime has already been taken")
563    }
564}
565
566impl<T: PetriVmRuntime> std::ops::Deref for PetriVmRuntimeGuard<T> {
567    type Target = T;
568
569    fn deref(&self) -> &T {
570        self.runtime
571            .as_ref()
572            .expect("runtime has already been taken")
573    }
574}
575
576impl<T: PetriVmRuntime> std::ops::DerefMut for PetriVmRuntimeGuard<T> {
577    fn deref_mut(&mut self) -> &mut T {
578        self.runtime
579            .as_mut()
580            .expect("runtime has already been taken")
581    }
582}
583
584impl<T: PetriVmRuntime> Drop for PetriVmRuntimeGuard<T> {
585    fn drop(&mut self) {
586        let Some(runtime) = self.runtime.take() else {
587            return;
588        };
589        if !self.capture_inspect_on_drop {
590            return;
591        }
592        let inspector = runtime.inspector();
593        let openhcl_diag_handler = runtime.openhcl_diag();
594        if inspector.is_none() && openhcl_diag_handler.is_none() {
595            return;
596        }
597        let log_source = self.log_source.clone();
598
599        let capture = async move {
600            let vmm = async {
601                if let Some(inspector) = &inspector {
602                    collect_inspect("vmm", inspector.inspect(""), &log_source, "failure").await;
603                }
604            };
605            let openhcl = async {
606                if let Some(diag) = &openhcl_diag_handler {
607                    collect_inspect(
608                        "openhcl",
609                        diag.inspect("", None, None),
610                        &log_source,
611                        "failure",
612                    )
613                    .await;
614                }
615            };
616            futures::future::join(vmm, openhcl).await;
617            drop(runtime);
618        };
619
620        // `SimpleTest::new_async` joins the test body with the task pool, so
621        // the pool keeps polling detached tasks until they complete. This
622        // finishes before the post-test hooks run.
623        self.driver
624            .spawn("petri-inspect-on-drop", async move {
625                // A panic here would propagate out of the task pool and replace
626                // the failure the test is already reporting.
627                if let Err(e) = AssertUnwindSafe(capture).catch_unwind().await {
628                    tracing::error!(?e, "panicked while collecting inspect state");
629                }
630            })
631            .detach();
632    }
633}
634
635impl<T: PetriVmmBackend> PetriVmBuilder<T> {
636    /// Create a new VM configuration.
637    pub fn new(
638        params: PetriTestParams<'_>,
639        artifacts: PetriVmArtifacts<T>,
640        driver: &DefaultDriver,
641    ) -> anyhow::Result<Self> {
642        let isolated_without_paravisor = artifacts.firmware.is_isolated_without_paravisor();
643        let builder = Self::minimal(params.test_name, params.log_source, artifacts, driver)?
644            .clear_minimal_mode()
645            .with_serial_output()
646            .with_capture_inspect_on_failure();
647        if isolated_without_paravisor {
648            Ok(builder)
649        } else {
650            builder
651                .add_petri_scsi_controllers()
652                .add_guest_crash_disk(params.post_test_hooks)
653        }
654    }
655
656    /// Create a minimal VM builder with only the bare minimum device set.
657    ///
658    /// Unlike [`new()`](Self::new), this constructor:
659    /// - Does not add default VMBus devices (shutdown IC, KVP, etc.)
660    /// - Does not add serial ports
661    /// - Does not add SCSI controllers or crash dump disks
662    /// - Does not verify save/restore on boot
663    ///
664    /// Use builder methods to opt in to specific devices. Intended for
665    /// performance tests where minimal overhead is critical.
666    pub fn minimal(
667        test_name: &str,
668        log_source: &PetriLogSource,
669        artifacts: PetriVmArtifacts<T>,
670        driver: &DefaultDriver,
671    ) -> anyhow::Result<Self> {
672        let isolated_without_paravisor = artifacts.firmware.is_isolated_without_paravisor();
673        match &artifacts.firmware {
674            Firmware::Uefi {
675                guest: UefiGuest::Mshv(_),
676                ..
677            } => anyhow::ensure!(
678                artifacts.arch == MachineArch::X86_64,
679                "mshv boot requires x86-64 UEFI"
680            ),
681            Firmware::OpenhclUefi {
682                guest: UefiGuest::Mshv(_),
683                ..
684            } => anyhow::bail!("mshv boot with OpenHCL is not supported"),
685            _ => {}
686        }
687        let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
688        let expected_boot_event = artifacts.firmware.expected_boot_event();
689        let boot_device_type = match artifacts.firmware {
690            Firmware::LinuxDirect { .. } | Firmware::IgvmLinuxDirect { .. } => BootDeviceType::None,
691            Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
692            Firmware::Pcat { .. } => BootDeviceType::Ide,
693            Firmware::OpenhclPcat { .. } => BootDeviceType::IdeViaScsi,
694            Firmware::Uefi {
695                guest: UefiGuest::None,
696                ..
697            }
698            | Firmware::OpenhclUefi {
699                guest: UefiGuest::None,
700                ..
701            } => BootDeviceType::None,
702            Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
703        };
704
705        let supports_file_sharing = T::supports_file_sharing(&artifacts.firmware);
706        let external_pipette = isolated_without_paravisor && artifacts.pipette_binary.is_some();
707        let builder = Self {
708            backend: artifacts.backend,
709            config: PetriVmConfig {
710                name: test_name.to_string(),
711                arch: artifacts.arch,
712                host_log_levels: None,
713                firmware: artifacts.firmware,
714                hibernation_enabled: false,
715                ipmi_enabled: false,
716                memory: Default::default(),
717                proc_topology: Default::default(),
718                nested_virt_enabled: false,
719
720                vmgs: PetriVmgsResource::Ephemeral,
721                tpm: None,
722                vmbus_storage_controllers: HashMap::new(),
723                pcie_nvme_drives: Vec::new(),
724                pcie_virtio_blk_drives: Vec::new(),
725                physical_nvme_devices: HashMap::new(),
726                guest_shares: HashMap::new(),
727            },
728            modify_vmm_config: None,
729            resources: PetriVmResources {
730                driver: driver.clone(),
731                log_source: log_source.clone(),
732                prebuilt_initrd: None,
733            },
734
735            guest_quirks,
736            vmm_quirks,
737            expected_boot_event,
738            override_expect_reset: false,
739
740            agent_image: artifacts.agent_image,
741            openhcl_agent_image: artifacts.openhcl_agent_image,
742            boot_device_type,
743            pcie_boot_port: None,
744
745            minimal_mode: true,
746            pipette_binary: artifacts.pipette_binary,
747            enable_serial: false,
748            use_virtio_console: false,
749            enable_screenshots: true,
750            use_virtio_vsock: isolated_without_paravisor,
751            #[cfg(target_os = "linux")]
752            vhost_vsock_guest_cid: None,
753            no_vmbus: !T::SUPPORTS_VMBUS || isolated_without_paravisor,
754            no_hv: false,
755            capture_inspect_on_failure: false,
756            watchdog_timeout: Some(Duration::from_mins(10)),
757            external_pipette: false,
758
759            nested_test: artifacts
760                .nested_test
761                .map(|t| t.resolve(!supports_file_sharing))
762                .transpose()?,
763        };
764        Ok(if external_pipette {
765            builder.with_external_pipette()
766        } else {
767            builder
768        })
769    }
770
771    fn clear_minimal_mode(mut self) -> Self {
772        self.minimal_mode = false;
773        self
774    }
775
776    /// Share a directory over 9p and mount it after Pipette connects.
777    ///
778    /// The backend attaches the filesystem device automatically.
779    pub fn with_shared_directory(
780        mut self,
781        tag: &str,
782        host_path: impl AsRef<Path>,
783        guest_path: &str,
784    ) -> Self {
785        assert!(
786            T::supports_file_sharing(&self.config.firmware),
787            "directory sharing requires a backend with Linux file-sharing support"
788        );
789        assert!(
790            !tag.is_empty()
791                && !tag.starts_with('-')
792                && tag
793                    .bytes()
794                    .all(|c| c.is_ascii_alphanumeric() || b"-_".contains(&c)),
795            "invalid filesystem share tag"
796        );
797        assert!(
798            guest_path.starts_with('/')
799                && !guest_path.contains('\0')
800                && !guest_path.split('/').any(|c| c == ".."),
801            "guest mount point must be an absolute path without parent traversal"
802        );
803        assert!(
804            !self.config.guest_shares.contains_key(tag),
805            "duplicate filesystem share tag: {tag}"
806        );
807        self.config.guest_shares.insert(
808            tag.into(),
809            GuestShare {
810                host_path: host_path.as_ref().to_path_buf(),
811                guest_path: guest_path.into(),
812            },
813        );
814        self
815    }
816
817    fn with_capture_inspect_on_failure(mut self) -> Self {
818        self.capture_inspect_on_failure = true;
819        self
820    }
821
822    /// Whether this builder is in minimal mode.
823    pub fn is_minimal(&self) -> bool {
824        self.minimal_mode
825    }
826
827    /// Set the diagnostic watchdog deadline. A standalone runner that owns its
828    /// own cancellation policy can disable the test watchdog with `None`.
829    pub fn with_watchdog_timeout(mut self, timeout: Option<Duration>) -> Self {
830        self.watchdog_timeout = timeout;
831        self
832    }
833
834    /// Use Pipette supplied and started by the guest image, without adding an
835    /// agent disk or injecting Pipette into a Linux-direct initrd.
836    ///
837    /// OpenVMM Linux-direct boot leaves the initrd's default entry point intact
838    /// unless [`Self::with_prebuilt_initrd`] supplies an explicit `rdinit`.
839    pub fn with_external_pipette(mut self) -> Self {
840        self.external_pipette = true;
841        self.agent_image = None;
842        self.pipette_binary = None;
843        self
844    }
845
846    /// Supply a pre-built initrd with pipette already injected.
847    ///
848    /// When set, the builder skips the runtime gzip decompress/inject/
849    /// recompress cycle, using this initrd directly. Use
850    /// [`prepare_initrd`](Self::prepare_initrd) to build the initrd
851    /// ahead of time.
852    pub fn with_prebuilt_initrd(mut self, initrd: PetriInitrd) -> Self {
853        self.resources.prebuilt_initrd = Some(initrd);
854        self
855    }
856
857    /// Pre-build the modified initrd with pipette injected.
858    ///
859    /// Reads the original initrd from the firmware artifacts, injects
860    /// the pipette binary via CPIO, and writes the result to a temp file.
861    /// Returns a [`PetriInitrd`] struct that contains a `TempFile` that
862    /// must not be dropped until after the VM boots.
863    ///
864    /// Call this once before timing, then pass the path to
865    /// [`with_prebuilt_initrd`](Self::with_prebuilt_initrd) for each
866    /// iteration.
867    pub fn prepare_initrd(&self) -> anyhow::Result<PetriInitrd> {
868        self.prepare_custom_initrd(T::build_custom_init_script)
869    }
870
871    /// Prepare an initrd with a custom script
872    pub fn prepare_custom_initrd(
873        &self,
874        build_custom_init_script: impl FnOnce(&str) -> Option<String>,
875    ) -> anyhow::Result<PetriInitrd> {
876        const PIPETTE_PATH: &str = "pipette";
877        const INIT_SCRIPT_NAME: &str = "custom-init.sh";
878
879        let initrd_path = self
880            .config
881            .firmware
882            .linux_direct_initrd()
883            .context("prepare_initrd requires Linux direct boot with initrd")?;
884        let pipette_path = self
885            .pipette_binary
886            .as_ref()
887            .context("prepare_initrd requires a pipette binary")?;
888
889        let pipette_data = std::fs::read(pipette_path.get()).with_context(|| {
890            format!(
891                "failed to read pipette binary at {}",
892                pipette_path.get().display()
893            )
894        })?;
895
896        let mut files = vec![crate::initrd::InitrdFile {
897            name: PIPETTE_PATH,
898            contents: &pipette_data,
899            permissions: 0o755,
900        }];
901        let script = build_custom_init_script(PIPETTE_PATH);
902        let rdinit = if let Some(script) = &script {
903            files.push(crate::initrd::InitrdFile {
904                name: INIT_SCRIPT_NAME,
905                contents: script.as_bytes(),
906                permissions: 0o755,
907            });
908            format!("/{INIT_SCRIPT_NAME}")
909        } else {
910            format!("/{PIPETTE_PATH}")
911        };
912        PetriInitrd::prepare(initrd_path, &files, &rdinit, None)
913    }
914
915    /// Enable serial port output even in minimal mode.
916    ///
917    /// Useful for diagnostics — the serial device overhead is negligible;
918    /// the cost comes from kernel console output, which is controlled via
919    /// the kernel cmdline (`quiet loglevel=0`).
920    ///
921    /// Note: this currently only affects LinuxDirect boot (kernel cmdline
922    /// and emulated serial backends). UEFI paths are unaffected.
923    pub fn with_serial_output(mut self) -> Self {
924        self.enable_serial = true;
925        self
926    }
927
928    /// Disable serial port output.
929    ///
930    /// Suppresses serial device creation, eliminating the `[uefi]` / `[openhcl]`
931    /// log lines. Useful for performance tests where serial noise is unwanted.
932    pub fn without_serial_output(mut self) -> Self {
933        self.enable_serial = false;
934        self
935    }
936
937    /// Disable periodic framebuffer screenshots.
938    ///
939    /// Suppresses the watchdog task that takes screenshots every 2 seconds,
940    /// eliminating the "No change in framebuffer" debug log lines.
941    pub fn without_screenshots(mut self) -> Self {
942        self.enable_screenshots = false;
943        self
944    }
945
946    /// Use virtio vsock instead of VMBus-based hvsocket for guest communication.
947    /// The virtio-vsock device will use PCIe, so a PCIe root topology must be
948    /// configured.
949    ///
950    /// When enabled, a virtio-vsock device is added to the VM. This device uses
951    /// the same Unix socket relay path that hvsocket would otherwise use, so
952    /// pipette will connect using this.
953    ///
954    /// For Linux direct boot, this also adjusts the kernel command line to
955    /// blacklist hv_sock instead of virtio_vsock.
956    ///
957    /// Disable it after [`Self::with_no_vmbus`] when selecting another transport.
958    pub fn with_virtio_vsock(mut self, enable: bool) -> Self {
959        self.use_virtio_vsock = enable;
960        #[cfg(target_os = "linux")]
961        {
962            self.vhost_vsock_guest_cid = None;
963        }
964        self
965    }
966
967    /// Use the Linux kernel vhost-vsock backend for guest communication.
968    ///
969    /// The host connects directly to the guest's `AF_VSOCK` listener at
970    /// `guest_cid`. The OpenVMM backend automatically uses shared guest memory,
971    /// which is required by kernel vhost.
972    #[cfg(target_os = "linux")]
973    pub fn with_vhost_vsock(mut self, guest_cid: u32) -> Self {
974        assert!(
975            (3..u32::MAX).contains(&guest_cid),
976            "vhost-vsock guest CID must be between 3 and {}",
977            u32::MAX - 1
978        );
979        self.use_virtio_vsock = true;
980        self.vhost_vsock_guest_cid = Some(guest_cid);
981        self
982    }
983
984    /// Disable VMBus entirely.
985    ///
986    /// This removes all VMBus storage controllers. For Linux guests,
987    /// virtio-vsock is used for pipette communication. For Windows guests,
988    /// the caller must also configure TCP pipette transport via
989    /// `modify_backend(|b| b.with_tcp_pipette_nic(port, mac_address))`. The
990    /// guest must boot from a non-VMBus device (e.g. PCIe NVMe).
991    pub fn with_no_vmbus(mut self) -> Self {
992        self.no_vmbus = true;
993        if self.config.firmware.os_flavor() != OsFlavor::Windows {
994            self.use_virtio_vsock = true;
995        }
996        self.config.vmbus_storage_controllers.clear();
997        self
998    }
999
1000    /// Disable the hypervisor (HV#1) enlightenments.
1001    ///
1002    /// This also disables VMBus, since VMBus depends on the hypervisor. On
1003    /// aarch64 UEFI this causes the loader to pass the generic SEC platform
1004    /// type to the firmware. This mode is not supported on x86_64 UEFI.
1005    pub fn with_no_hv(mut self) -> Self {
1006        self.no_hv = true;
1007        self.with_no_vmbus()
1008    }
1009
1010    fn add_petri_scsi_controllers(mut self) -> Self {
1011        if T::SUPPORTS_VMBUS {
1012            self = self.add_vmbus_storage_controller(
1013                &PETRI_SCSI_VTL0_CONTROLLER,
1014                Vtl::Vtl0,
1015                VmbusStorageType::Scsi,
1016            );
1017
1018            if self.is_openhcl() {
1019                self = self.add_vmbus_storage_controller(
1020                    &PETRI_SCSI_VTL2_CONTROLLER,
1021                    Vtl::Vtl2,
1022                    VmbusStorageType::Scsi,
1023                )
1024            }
1025        }
1026        self
1027    }
1028
1029    fn add_guest_crash_disk(
1030        self,
1031        post_test_hooks: &mut Vec<PetriPostTestHook>,
1032    ) -> anyhow::Result<Self> {
1033        let supports_vmbus_crash_disk = !self.no_vmbus
1034            && matches!(
1035                self.config.firmware.os_flavor(),
1036                OsFlavor::Windows | OsFlavor::Linux
1037            );
1038
1039        let (disk, disk_hook) = supports_vmbus_crash_disk
1040            .then(|| T::create_guest_dump_disk().context("failed to create guest dump disk"))
1041            .transpose()?
1042            .flatten()
1043            .unzip();
1044
1045        if let Some(disk_hook) = disk_hook {
1046            let logger = self.resources.log_source.clone();
1047            post_test_hooks.push(PetriPostTestHook::new(
1048                "extract guest crash dumps".into(),
1049                move |test_passed| {
1050                    if test_passed {
1051                        return Ok(());
1052                    }
1053                    let mut disk = disk_hook()?;
1054                    let gpt = gptman::GPT::read_from(&mut disk, SECTOR_SIZE)?;
1055                    let partition = fscommon::StreamSlice::new(
1056                        &mut disk,
1057                        gpt[1].starting_lba * SECTOR_SIZE,
1058                        gpt[1].ending_lba * SECTOR_SIZE,
1059                    )?;
1060                    let fs = fatfs::FileSystem::new(partition, fatfs::FsOptions::new())?;
1061                    for entry in fs.root_dir().iter() {
1062                        let Ok(entry) = entry else {
1063                            tracing::warn!(?entry, "failed to read entry in guest crash dump disk");
1064                            continue;
1065                        };
1066                        if !entry.is_file() {
1067                            tracing::warn!(
1068                                ?entry,
1069                                "skipping non-file entry in guest crash dump disk"
1070                            );
1071                            continue;
1072                        }
1073                        logger.write_attachment(&entry.file_name(), entry.to_file())?;
1074                    }
1075                    Ok(())
1076                },
1077            ));
1078        }
1079
1080        Ok(if let Some(disk) = disk {
1081            self.add_vmbus_drive(
1082                Drive::new(Some(Disk::Temporary(disk)), false),
1083                &PETRI_SCSI_VTL0_CONTROLLER,
1084                Some(PETRI_SCSI_CRASH_LUN),
1085            )
1086        } else {
1087            self
1088        })
1089    }
1090
1091    fn add_agent_disks(self) -> anyhow::Result<Self> {
1092        self.add_agent_disk_inner(Vtl::Vtl0)?
1093            .add_agent_disk_inner(Vtl::Vtl2)
1094    }
1095
1096    fn add_agent_disk_inner(mut self, target_vtl: Vtl) -> anyhow::Result<Self> {
1097        let (agent_image, controller_id) = match target_vtl {
1098            Vtl::Vtl0 => (self.agent_image.as_ref(), PETRI_SCSI_VTL0_CONTROLLER),
1099            Vtl::Vtl1 => panic!("no VTL1 agent disk"),
1100            Vtl::Vtl2 => (
1101                self.openhcl_agent_image.as_ref(),
1102                PETRI_SCSI_VTL2_CONTROLLER,
1103            ),
1104        };
1105
1106        // When using pipette-as-init, the VTL0 agent disk is only needed
1107        // if it carries extra files (pipette itself is in the initrd).
1108        if target_vtl == Vtl::Vtl0
1109            && self.uses_pipette_as_init()
1110            && !agent_image.is_some_and(|i| i.has_extras())
1111        {
1112            return Ok(self);
1113        }
1114
1115        let Some(agent_disk) = agent_image
1116            .map(|i| {
1117                i.build(crate::disk_image::ImageType::Vhd)
1118                    .context("failed to build agent image")
1119            })
1120            .transpose()?
1121            .flatten()
1122        else {
1123            return Ok(self);
1124        };
1125
1126        // When VMBus is disabled, route the agent disk through PCIe NVMe
1127        // instead of VMBus SCSI.
1128        if self.no_vmbus {
1129            self.config.pcie_nvme_drives.push(PcieNvmeDrive {
1130                port_name: PETRI_PCIE_NVME_AGENT_PORT.into(),
1131                nsid: PETRI_PCIE_NVME_AGENT_NSID,
1132                drive: Drive::new(
1133                    Some(Disk::Temporary(Arc::new(agent_disk.into_temp_path()))),
1134                    false,
1135                ),
1136            });
1137            return Ok(self);
1138        }
1139
1140        // Ensure the storage controller exists (minimal mode doesn't
1141        // add controllers upfront).
1142        if !self
1143            .config
1144            .vmbus_storage_controllers
1145            .contains_key(&controller_id)
1146        {
1147            self = self.add_vmbus_storage_controller(
1148                &controller_id,
1149                target_vtl,
1150                VmbusStorageType::Scsi,
1151            );
1152        }
1153
1154        Ok(self.add_vmbus_drive(
1155            Drive::new(
1156                Some(Disk::Temporary(Arc::new(agent_disk.into_temp_path()))),
1157                false,
1158            ),
1159            &controller_id,
1160            Some(PETRI_SCSI_PIPETTE_LUN),
1161        ))
1162    }
1163
1164    fn add_boot_disk(mut self) -> anyhow::Result<Self> {
1165        if self.boot_device_type.requires_vtl2() && !self.is_openhcl() {
1166            panic!("boot device type {:?} requires vtl2", self.boot_device_type);
1167        }
1168
1169        if self.no_vmbus && self.boot_device_type.requires_vmbus() {
1170            panic!(
1171                "boot device type {:?} requires vmbus, but vmbus is disabled; \
1172                 use with_boot_device_type(BootDeviceType::PcieNvme) or similar",
1173                self.boot_device_type
1174            );
1175        }
1176
1177        if self.boot_device_type.requires_vpci_boot() {
1178            self.config
1179                .firmware
1180                .uefi_config_mut()
1181                .expect("vpci boot requires uefi")
1182                .enable_vpci_boot = true;
1183        }
1184
1185        let builder = if let Some(boot_drive) =
1186            self.config.firmware.boot_drive(self.use_virtio_vsock)?
1187        {
1188            match self.boot_device_type {
1189                BootDeviceType::None => unreachable!(),
1190                BootDeviceType::Ide => self.add_ide_drive(
1191                    boot_drive,
1192                    PETRI_IDE_BOOT_CONTROLLER_NUMBER,
1193                    PETRI_IDE_BOOT_LUN,
1194                ),
1195                BootDeviceType::IdeViaScsi => self
1196                    .add_vmbus_drive(
1197                        boot_drive,
1198                        &PETRI_SCSI_VTL2_CONTROLLER,
1199                        Some(PETRI_SCSI_BOOT_LUN),
1200                    )
1201                    .add_vtl2_storage_controller(
1202                        Vtl2StorageControllerBuilder::new(ControllerType::Ide)
1203                            .with_instance_id(PETRI_IDE_BOOT_CONTROLLER)
1204                            .add_lun(
1205                                Vtl2LunBuilder::disk()
1206                                    .with_channel(PETRI_IDE_BOOT_CONTROLLER_NUMBER)
1207                                    .with_location(PETRI_IDE_BOOT_LUN as u32)
1208                                    .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
1209                                        ControllerType::Scsi,
1210                                        PETRI_SCSI_VTL2_CONTROLLER,
1211                                        PETRI_SCSI_BOOT_LUN,
1212                                    )),
1213                            )
1214                            .build(),
1215                    ),
1216                BootDeviceType::IdeViaNvme => todo!(),
1217                BootDeviceType::Scsi => self.add_vmbus_drive(
1218                    boot_drive,
1219                    &PETRI_SCSI_VTL0_CONTROLLER,
1220                    Some(PETRI_SCSI_BOOT_LUN),
1221                ),
1222                BootDeviceType::ScsiViaScsi => self
1223                    .add_vmbus_drive(
1224                        boot_drive,
1225                        &PETRI_SCSI_VTL2_CONTROLLER,
1226                        Some(PETRI_SCSI_BOOT_LUN),
1227                    )
1228                    .add_vtl2_storage_controller(
1229                        Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
1230                            .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
1231                            .add_lun(
1232                                Vtl2LunBuilder::disk()
1233                                    .with_location(PETRI_SCSI_BOOT_LUN)
1234                                    .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
1235                                        ControllerType::Scsi,
1236                                        PETRI_SCSI_VTL2_CONTROLLER,
1237                                        PETRI_SCSI_BOOT_LUN,
1238                                    )),
1239                            )
1240                            .build(),
1241                    ),
1242                BootDeviceType::ScsiViaNvme => self
1243                    .add_vmbus_storage_controller(
1244                        &PETRI_NVME_BOOT_VTL2_CONTROLLER,
1245                        Vtl::Vtl2,
1246                        VmbusStorageType::Nvme,
1247                    )
1248                    .add_vmbus_drive(
1249                        boot_drive,
1250                        &PETRI_NVME_BOOT_VTL2_CONTROLLER,
1251                        Some(PETRI_NVME_BOOT_NSID),
1252                    )
1253                    .add_vtl2_storage_controller(
1254                        Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
1255                            .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
1256                            .add_lun(
1257                                Vtl2LunBuilder::disk()
1258                                    .with_location(PETRI_SCSI_BOOT_LUN)
1259                                    .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
1260                                        ControllerType::Nvme,
1261                                        PETRI_NVME_BOOT_VTL2_CONTROLLER,
1262                                        PETRI_NVME_BOOT_NSID,
1263                                    )),
1264                            )
1265                            .build(),
1266                    ),
1267                BootDeviceType::Nvme => self
1268                    .add_vmbus_storage_controller(
1269                        &PETRI_NVME_BOOT_VTL0_CONTROLLER,
1270                        Vtl::Vtl0,
1271                        VmbusStorageType::Nvme,
1272                    )
1273                    .add_vmbus_drive(
1274                        boot_drive,
1275                        &PETRI_NVME_BOOT_VTL0_CONTROLLER,
1276                        Some(PETRI_NVME_BOOT_NSID),
1277                    ),
1278                BootDeviceType::NvmeViaScsi => todo!(),
1279                BootDeviceType::NvmeViaNvme => todo!(),
1280                BootDeviceType::PcieNvme => {
1281                    let port_name = self
1282                        .pcie_boot_port
1283                        .clone()
1284                        .unwrap_or_else(|| "s0rc0rp0".into());
1285                    self.config.pcie_nvme_drives.push(PcieNvmeDrive {
1286                        port_name,
1287                        nsid: 1,
1288                        drive: boot_drive,
1289                    });
1290                    self
1291                }
1292                BootDeviceType::PcieVirtioBlk => {
1293                    self.config.pcie_virtio_blk_drives.push(PcieVirtioBlkDrive {
1294                        port_name: self
1295                            .pcie_boot_port
1296                            .clone()
1297                            .unwrap_or_else(|| "s0rc0rp0".into()),
1298                        drive: boot_drive,
1299                    });
1300                    self
1301                }
1302            }
1303        } else {
1304            self
1305        };
1306        Ok(builder)
1307    }
1308
1309    /// Whether the VTL0 agent disk will actually be added.
1310    ///
1311    /// False when using pipette-as-init with no extra files (pipette is
1312    /// in the initrd, so the CIDATA disk isn't needed).
1313    fn has_agent_disk(&self) -> bool {
1314        if self.uses_pipette_as_init() {
1315            self.agent_image.as_ref().is_some_and(|i| i.has_extras())
1316        } else {
1317            self.agent_image.is_some()
1318        }
1319    }
1320
1321    /// Get properties about the vm for convenience
1322    pub fn properties(&self) -> PetriVmProperties {
1323        PetriVmProperties {
1324            is_openhcl: self.config.firmware.is_openhcl(),
1325            is_isolated: self.config.firmware.isolation().is_some(),
1326            is_pcat: self.config.firmware.is_pcat(),
1327            is_linux_direct: self.config.firmware.is_linux_direct(),
1328            using_vtl0_pipette: self.using_vtl0_pipette(),
1329            external_pipette: self.external_pipette,
1330            using_vpci: self.boot_device_type.requires_vpci_boot(),
1331            os_flavor: self.config.firmware.os_flavor(),
1332            minimal_mode: self.minimal_mode,
1333            uses_pipette_as_init: self.uses_pipette_as_init(),
1334            enable_serial: self.enable_serial,
1335            use_virtio_console: self.use_virtio_console,
1336            has_agent_disk: self.has_agent_disk(),
1337            use_virtio_vsock: self.use_virtio_vsock,
1338            #[cfg(target_os = "linux")]
1339            vhost_vsock_guest_cid: self.vhost_vsock_guest_cid,
1340            no_vmbus: self.no_vmbus,
1341            no_hv: self.no_hv,
1342        }
1343    }
1344
1345    /// Whether Petri will inject Pipette as PID 1 init in the initrd.
1346    ///
1347    /// True for non-OpenHCL Linux direct boot when a pipette binary is
1348    /// available. Pipette is injected into the initrd via CPIO and set
1349    /// as `rdinit=/pipette`.
1350    fn uses_pipette_as_init(&self) -> bool {
1351        self.config.firmware.is_linux_direct()
1352            && !self.config.firmware.is_openhcl()
1353            && self.pipette_binary.is_some()
1354    }
1355
1356    /// Whether this VM is using pipette in VTL0
1357    pub fn using_vtl0_pipette(&self) -> bool {
1358        self.external_pipette
1359            || self.uses_pipette_as_init()
1360            || self
1361                .agent_image
1362                .as_ref()
1363                .is_some_and(|x| x.contains_pipette())
1364    }
1365
1366    /// Build and run the VM, then wait for the VM to emit the expected boot
1367    /// event (if configured). Does not configure and start pipette. Should
1368    /// only be used for testing platforms that pipette does not support.
1369    pub async fn run_without_agent(self) -> anyhow::Result<PetriVm<T>> {
1370        self.run_core().await
1371    }
1372
1373    /// Build and run the VM, then wait for the VM to emit the expected boot
1374    /// event (if configured). Launches pipette and returns a client to it.
1375    pub async fn run(self) -> anyhow::Result<(PetriVm<T>, PipetteClient)> {
1376        assert!(self.using_vtl0_pipette());
1377
1378        let mut vm = self.add_nested_artifacts_to_agent_disk().run_core().await?;
1379        let client = vm.wait_for_agent().await?;
1380
1381        Ok((vm, client))
1382    }
1383
1384    async fn run_core(mut self) -> anyhow::Result<PetriVm<T>> {
1385        // Add the boot disk now to allow the test to modify the boot type
1386        // Add the agent disks now to allow the test to add custom files
1387        self = self.add_boot_disk()?.add_agent_disks()?;
1388
1389        // Auto-prepare the initrd with pipette injected if needed.
1390        // This centralizes the injection logic so backends only ever
1391        // receive a prebuilt_initrd path.
1392        if self.uses_pipette_as_init() && self.resources.prebuilt_initrd.is_none() {
1393            self.resources.prebuilt_initrd = Some(self.prepare_initrd()?);
1394        }
1395
1396        tracing::debug!(builder = ?self);
1397
1398        let arch = self.config.arch;
1399        let expect_reset = self.expect_reset();
1400        let properties = self.properties();
1401        let guest_shares = self.config.guest_shares.clone();
1402        let name = self.config.name.clone();
1403
1404        let (mut runtime, config) = self
1405            .backend
1406            .run(
1407                self.config,
1408                self.modify_vmm_config,
1409                &self.resources,
1410                &properties,
1411            )
1412            .await?;
1413        let openhcl_diag_handler = runtime.openhcl_diag();
1414        let watchdog_tasks = Self::start_watchdog_tasks(
1415            &self.resources,
1416            &mut runtime,
1417            self.enable_screenshots,
1418            self.watchdog_timeout,
1419        )?;
1420
1421        let mut vm = PetriVm {
1422            runtime: PetriVmRuntimeGuard {
1423                runtime: Some(runtime),
1424                driver: self.resources.driver.clone(),
1425                log_source: self.resources.log_source.clone(),
1426                capture_inspect_on_drop: self.capture_inspect_on_failure,
1427            },
1428            resources: self.resources,
1429            watchdog_tasks,
1430            openhcl_diag_handler,
1431
1432            arch,
1433            guest_quirks: self.guest_quirks,
1434            vmm_quirks: self.vmm_quirks,
1435            expected_boot_event: self.expected_boot_event,
1436
1437            config,
1438            properties,
1439
1440            nested_test: self.nested_test,
1441            guest_shares,
1442            name,
1443        };
1444
1445        if expect_reset {
1446            vm.wait_for_reset_core().await?;
1447        }
1448
1449        vm.wait_for_expected_boot_event().await?;
1450
1451        Ok(vm)
1452    }
1453
1454    fn expect_reset(&self) -> bool {
1455        self.override_expect_reset
1456            || matches!(
1457                (
1458                    self.guest_quirks.initial_reboot,
1459                    self.expected_boot_event,
1460                    &self.config.firmware,
1461                    &self.config.tpm,
1462                ),
1463                (
1464                    Some(InitialRebootCondition::Always),
1465                    Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
1466                    _,
1467                    _,
1468                ) | (
1469                    Some(InitialRebootCondition::WithTpm),
1470                    Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
1471                    _,
1472                    Some(_),
1473                )
1474            )
1475    }
1476
1477    fn start_watchdog_timer(
1478        driver: &DefaultDriver,
1479        watchdog_timeout: Option<Duration>,
1480    ) -> Option<Task<()>> {
1481        watchdog_timeout.map(|timeout| {
1482            // Unwind out of the task pool so Petri records a test failure.
1483            let timer_driver = driver.clone();
1484            driver.spawn("timer-watchdog", async move {
1485                PolledTimer::new(&timer_driver).sleep(timeout).await;
1486                tracing::error!(?timeout, "Test timeout reached, aborting.");
1487                panic!("Test timed out");
1488            })
1489        })
1490    }
1491
1492    fn start_watchdog_tasks(
1493        resources: &PetriVmResources,
1494        runtime: &mut T::VmRuntime,
1495        enable_screenshots: bool,
1496        watchdog_timeout: Option<Duration>,
1497    ) -> anyhow::Result<Vec<Task<()>>> {
1498        let mut tasks = Vec::new();
1499
1500        tasks.extend(Self::start_watchdog_timer(
1501            &resources.driver,
1502            watchdog_timeout,
1503        ));
1504
1505        if enable_screenshots {
1506            if let Some(mut framebuffer_access) = runtime.take_framebuffer_access() {
1507                let mut timer = PolledTimer::new(&resources.driver);
1508                let log_source = resources.log_source.clone();
1509
1510                tasks.push(
1511                    resources
1512                        .driver
1513                        .spawn("petri-watchdog-screenshot", async move {
1514                            let mut image = Vec::new();
1515                            let mut last_image = Vec::new();
1516                            loop {
1517                                timer.sleep(Duration::from_secs(2)).await;
1518                                tracing::trace!("Taking screenshot.");
1519
1520                                let VmScreenshotMeta {
1521                                    color,
1522                                    width,
1523                                    height,
1524                                } = match framebuffer_access.screenshot(&mut image).await {
1525                                    Ok(Some(meta)) => meta,
1526                                    Ok(None) => {
1527                                        tracing::debug!("VM off, skipping screenshot.");
1528                                        continue;
1529                                    }
1530                                    Err(e) => {
1531                                        tracing::error!(?e, "Failed to take screenshot");
1532                                        continue;
1533                                    }
1534                                };
1535
1536                                if image == last_image {
1537                                    tracing::debug!(
1538                                        "No change in framebuffer, skipping screenshot."
1539                                    );
1540                                    continue;
1541                                }
1542
1543                                let r = log_source.create_attachment("screenshot.png").and_then(
1544                                    |mut f| {
1545                                        image::write_buffer_with_format(
1546                                            &mut f,
1547                                            &image,
1548                                            width.into(),
1549                                            height.into(),
1550                                            color,
1551                                            image::ImageFormat::Png,
1552                                        )
1553                                        .map_err(Into::into)
1554                                    },
1555                                );
1556
1557                                if let Err(e) = r {
1558                                    tracing::error!(?e, "Failed to save screenshot");
1559                                } else {
1560                                    tracing::info!("Screenshot saved.");
1561                                }
1562
1563                                std::mem::swap(&mut image, &mut last_image);
1564                            }
1565                        }),
1566                );
1567            }
1568        }
1569
1570        Ok(tasks)
1571    }
1572
1573    /// Configure the test to expect a boot failure from the VM.
1574    /// Useful for negative tests.
1575    pub fn with_expect_boot_failure(mut self) -> Self {
1576        self.expected_boot_event = Some(FirmwareEvent::BootFailed);
1577        self
1578    }
1579
1580    /// Configure the test to not expect any boot event.
1581    /// Useful for tests that do not boot a VTL0 guest.
1582    pub fn with_expect_no_boot_event(mut self) -> Self {
1583        self.expected_boot_event = None;
1584        self
1585    }
1586
1587    /// Allow the VM to reset once at the beginning of the test. Should only be
1588    /// used if you are using a special VM configuration that causes the guest
1589    /// to reboot when it usually wouldn't.
1590    pub fn with_expect_reset(mut self) -> Self {
1591        self.override_expect_reset = true;
1592        self
1593    }
1594
1595    /// Set the VM to enable secure boot and inject the templates per OS flavor.
1596    pub fn with_secure_boot(mut self) -> Self {
1597        self.config
1598            .firmware
1599            .uefi_config_mut()
1600            .expect("Secure boot is only supported for UEFI firmware.")
1601            .secure_boot_enabled = true;
1602
1603        match self.os_flavor() {
1604            OsFlavor::Windows => self.with_windows_secure_boot_template(),
1605            OsFlavor::Linux => self.with_uefi_ca_secure_boot_template(),
1606            _ => panic!(
1607                "Secure boot unsupported for OS flavor {:?}",
1608                self.os_flavor()
1609            ),
1610        }
1611    }
1612
1613    /// Inject Windows secure boot templates into the VM's UEFI.
1614    pub fn with_windows_secure_boot_template(mut self) -> Self {
1615        self.config
1616            .firmware
1617            .uefi_config_mut()
1618            .expect("Secure boot is only supported for UEFI firmware.")
1619            .secure_boot_template = Some(SecureBootTemplate::MicrosoftWindows);
1620        self
1621    }
1622
1623    /// Inject UEFI CA secure boot templates into the VM's UEFI.
1624    pub fn with_uefi_ca_secure_boot_template(mut self) -> Self {
1625        self.config
1626            .firmware
1627            .uefi_config_mut()
1628            .expect("Secure boot is only supported for UEFI firmware.")
1629            .secure_boot_template = Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority);
1630        self
1631    }
1632
1633    /// Apply a custom UEFI variable delta encoded as JSON.
1634    pub fn with_custom_uefi_json(mut self, json: impl Into<Vec<u8>>) -> Self {
1635        self.config
1636            .firmware
1637            .uefi_config_mut()
1638            .expect("Custom UEFI variables are only supported for UEFI firmware.")
1639            .custom_uefi_json = Some(json.into());
1640        self
1641    }
1642
1643    /// Set the VM to use the specified processor topology.
1644    pub fn with_processor_topology(mut self, topology: ProcessorTopology) -> Self {
1645        self.config.proc_topology = topology;
1646        self
1647    }
1648
1649    /// Set the VM to use the specified memory config.
1650    pub fn with_memory(mut self, memory: MemoryConfig) -> Self {
1651        self.config.memory = memory;
1652        self
1653    }
1654
1655    /// Sets a custom OpenHCL IGVM VTL2 address type. This controls the behavior
1656    /// of where VTL2 is placed in address space, and also the total size of memory
1657    /// allocated for VTL2. VTL2 start will fail if `address_type` is specified
1658    /// and leads to the loader allocating less memory than what is in the IGVM file.
1659    pub fn with_vtl2_base_address_type(mut self, address_type: Vtl2BaseAddressType) -> Self {
1660        self.config
1661            .firmware
1662            .openhcl_config_mut()
1663            .expect("OpenHCL firmware is required to set custom VTL2 address type.")
1664            .vtl2_base_address_type = Some(address_type);
1665        self
1666    }
1667
1668    /// Sets a custom OpenHCL IGVM file to use.
1669    pub fn with_custom_openhcl(mut self, artifact: ResolvedArtifact<impl IsOpenhclIgvm>) -> Self {
1670        match &mut self.config.firmware {
1671            Firmware::OpenhclLinuxDirect { igvm_firmware, .. }
1672            | Firmware::OpenhclPcat { igvm_firmware, .. }
1673            | Firmware::OpenhclUefi { igvm_firmware, .. } => {
1674                *igvm_firmware = IgvmFirmwareSource::File(artifact.erase());
1675            }
1676            Firmware::LinuxDirect { .. }
1677            | Firmware::IgvmLinuxDirect { .. }
1678            | Firmware::Uefi { .. }
1679            | Firmware::Pcat { .. } => {
1680                panic!("Custom OpenHCL is only supported for OpenHCL firmware.")
1681            }
1682        }
1683        self
1684    }
1685
1686    /// Load OpenHCL from VMGS, falling back to in-box firmware (Hyper-V only).
1687    pub fn with_openhcl_from_vmgs(mut self) -> Self {
1688        match &mut self.config.firmware {
1689            Firmware::OpenhclLinuxDirect { igvm_firmware, .. }
1690            | Firmware::OpenhclPcat { igvm_firmware, .. }
1691            | Firmware::OpenhclUefi { igvm_firmware, .. } => {
1692                *igvm_firmware = IgvmFirmwareSource::VmgsOrInBox;
1693            }
1694            Firmware::LinuxDirect { .. }
1695            | Firmware::IgvmLinuxDirect { .. }
1696            | Firmware::Uefi { .. }
1697            | Firmware::Pcat { .. } => {
1698                panic!("Loading OpenHCL from VMGS is only supported for OpenHCL firmware.")
1699            }
1700        }
1701        self
1702    }
1703
1704    /// Append additional command line arguments to pass to the paravisor.
1705    pub fn with_openhcl_command_line(mut self, additional_command_line: &str) -> Self {
1706        append_cmdline(
1707            &mut self
1708                .config
1709                .firmware
1710                .openhcl_config_mut()
1711                .expect("OpenHCL command line is only supported for OpenHCL firmware.")
1712                .custom_command_line,
1713            additional_command_line,
1714        );
1715        self
1716    }
1717
1718    /// Configure whether OpenHCL enables MANA keepalive at boot.
1719    pub fn with_mana_keepalive(mut self, enable: bool) -> Self {
1720        self.config
1721            .firmware
1722            .openhcl_config_mut()
1723            .expect("MANA keepalive is only supported for OpenHCL firmware.")
1724            .enable_mana_keepalive = enable;
1725        self
1726    }
1727
1728    /// Enable confidential filtering, even if the VM is not confidential.
1729    pub fn with_confidential_filtering(self) -> Self {
1730        if !self.config.firmware.is_openhcl() {
1731            panic!("Confidential filtering is only supported for OpenHCL");
1732        }
1733        self.with_openhcl_command_line(&format!(
1734            "{}=1 {}=0",
1735            underhill_confidentiality::OPENHCL_CONFIDENTIAL_ENV_VAR_NAME,
1736            underhill_confidentiality::OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME
1737        ))
1738    }
1739
1740    /// Sets the command line parameters passed to OpenHCL related to logging.
1741    pub fn with_openhcl_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1742        self.config
1743            .firmware
1744            .openhcl_config_mut()
1745            .expect("OpenHCL firmware is required to set custom OpenHCL log levels.")
1746            .log_levels = levels;
1747        self
1748    }
1749
1750    /// Sets the log levels for the host OpenVMM process.
1751    /// DEVNOTE: In the future, this could be generalized for both HyperV and OpenVMM.
1752    /// For now, this is only implemented for OpenVMM.
1753    pub fn with_host_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1754        if let OpenvmmLogConfig::Custom(ref custom_levels) = levels {
1755            for key in custom_levels.keys() {
1756                if !["OPENVMM_LOG", "OPENVMM_SHOW_SPANS"].contains(&key.as_str()) {
1757                    panic!("Unsupported OpenVMM log level key: {}", key);
1758                }
1759            }
1760        }
1761
1762        self.config.host_log_levels = Some(levels.clone());
1763        self
1764    }
1765
1766    /// Adds a file to the VM's pipette agent image.
1767    pub fn with_agent_file(mut self, name: &str, file_path: impl AsRef<Path>) -> Self {
1768        self.agent_image
1769            .as_mut()
1770            .expect("no guest pipette")
1771            .add_file(name, file_path);
1772        self
1773    }
1774
1775    /// Adds a file to the paravisor's pipette agent image.
1776    pub fn with_openhcl_agent_file(mut self, name: &str, file_path: impl AsRef<Path>) -> Self {
1777        self.openhcl_agent_image
1778            .as_mut()
1779            .expect("no openhcl pipette")
1780            .add_file(name, file_path);
1781        self
1782    }
1783
1784    /// Sets whether UEFI frontpage is enabled.
1785    pub fn with_uefi_frontpage(mut self, enable: bool) -> Self {
1786        self.config
1787            .firmware
1788            .uefi_config_mut()
1789            .expect("UEFI frontpage is only supported for UEFI firmware.")
1790            .disable_frontpage = !enable;
1791        self
1792    }
1793
1794    /// Sets the UEFI diagnostics log level filter.
1795    ///
1796    /// By default only ERROR and WARN level entries are forwarded to the
1797    /// host tracing infrastructure. Use this to also surface INFO (or all)
1798    /// entries when a test needs to observe them.
1799    pub fn with_efi_diagnostics_log_level(mut self, level: EfiDiagnosticsLogLevel) -> Self {
1800        self.config
1801            .firmware
1802            .uefi_config_mut()
1803            .expect("EFI diagnostics log level is only supported for UEFI firmware.")
1804            .efi_diagnostics_log_level = level;
1805        self
1806    }
1807
1808    /// Sets the per-period rate-limit override for UEFI diagnostics emission.
1809    ///
1810    /// - Not called: use the built-in defaults.
1811    /// - `0`: disable rate limiting entirely (emit every entry).
1812    /// - `n > 0`: use `n` as the per-period limit.
1813    pub fn with_efi_diagnostics_rate_limit(mut self, limit: u32) -> Self {
1814        self.config
1815            .firmware
1816            .uefi_config_mut()
1817            .expect("EFI diagnostics rate limit is only supported for UEFI firmware.")
1818            .efi_diagnostics_rate_limit = Some(limit);
1819        self
1820    }
1821
1822    /// Sets whether UEFI should always attempt a default boot.
1823    pub fn with_default_boot_always_attempt(mut self, enable: bool) -> Self {
1824        self.config
1825            .firmware
1826            .uefi_config_mut()
1827            .expect("Default boot always attempt is only supported for UEFI firmware.")
1828            .default_boot_always_attempt = enable;
1829        self
1830    }
1831
1832    /// Force UEFI to bounce-buffer all DMA traffic.
1833    pub fn with_uefi_force_dma_bounce(mut self, enable: bool) -> Self {
1834        self.config
1835            .firmware
1836            .uefi_config_mut()
1837            .expect("force DMA bounce is only supported for UEFI firmware.")
1838            .force_dma_bounce = enable;
1839        self
1840    }
1841
1842    /// Run the VM with Enable VMBus relay enabled
1843    pub fn with_vmbus_redirect(mut self, enable: bool) -> Self {
1844        self.config
1845            .firmware
1846            .openhcl_config_mut()
1847            .expect("VMBus redirection is only supported for OpenHCL firmware.")
1848            .vmbus_redirect = enable;
1849        self
1850    }
1851
1852    /// Enable guest hibernation support.
1853    ///
1854    /// Applies to any firmware type: for OpenHCL this sets the DPS
1855    /// `enable_hibernation` flag; for OpenVMM UEFI/PCAT firmware it enables the
1856    /// firmware's hibernation support.
1857    pub fn with_hibernation_enabled(mut self, enable: bool) -> Self {
1858        self.config.hibernation_enabled = enable;
1859        self
1860    }
1861
1862    /// Enable the IPMI KCS interface for an OpenHCL UEFI VM.
1863    pub fn with_ipmi(mut self, enable: bool) -> Self {
1864        self.config.ipmi_enabled = enable;
1865        self
1866    }
1867
1868    /// Specify the guest state lifetime for the VM
1869    pub fn with_guest_state_lifetime(
1870        mut self,
1871        guest_state_lifetime: PetriGuestStateLifetime,
1872    ) -> Self {
1873        let disk = match self.config.vmgs {
1874            PetriVmgsResource::Disk(disk)
1875            | PetriVmgsResource::ReprovisionOnFailure(disk)
1876            | PetriVmgsResource::ReprovisionSingle(disk) => disk,
1877            PetriVmgsResource::Ephemeral => PetriVmgsDisk::default(),
1878        };
1879        self.config.vmgs = match guest_state_lifetime {
1880            PetriGuestStateLifetime::Disk => PetriVmgsResource::Disk(disk),
1881            PetriGuestStateLifetime::ReprovisionOnFailure => {
1882                PetriVmgsResource::ReprovisionOnFailure(disk)
1883            }
1884            PetriGuestStateLifetime::ReprovisionSingle => {
1885                PetriVmgsResource::ReprovisionSingle(disk)
1886            }
1887            PetriGuestStateLifetime::Ephemeral => {
1888                if !matches!(disk.disk, Disk::Memory(_)) {
1889                    panic!("attempted to use ephemeral guest state after specifying backing vmgs")
1890                }
1891                PetriVmgsResource::Ephemeral
1892            }
1893        };
1894        self
1895    }
1896
1897    /// Specify the guest state encryption policy for the VM
1898    pub fn with_guest_state_encryption(mut self, policy: GuestStateEncryptionPolicy) -> Self {
1899        match &mut self.config.vmgs {
1900            PetriVmgsResource::Disk(vmgs)
1901            | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1902            | PetriVmgsResource::ReprovisionSingle(vmgs) => {
1903                vmgs.encryption_policy = policy;
1904            }
1905            PetriVmgsResource::Ephemeral => {
1906                panic!("attempted to encrypt ephemeral guest state")
1907            }
1908        }
1909        self
1910    }
1911
1912    /// Use the specified backing VMGS file
1913    pub fn with_initial_vmgs(self, disk: ResolvedArtifact<impl IsTestVmgs>) -> Self {
1914        self.with_backing_vmgs(Disk::Differencing(DiskPath::Local(disk.into())))
1915    }
1916
1917    /// Use the specified backing VMGS file
1918    pub fn with_persistent_vmgs(self, disk: impl AsRef<Path>) -> Self {
1919        self.with_backing_vmgs(Disk::Persistent(disk.as_ref().to_path_buf()))
1920    }
1921
1922    fn with_backing_vmgs(mut self, disk: Disk) -> Self {
1923        match &mut self.config.vmgs {
1924            PetriVmgsResource::Disk(vmgs)
1925            | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1926            | PetriVmgsResource::ReprovisionSingle(vmgs) => {
1927                if !matches!(vmgs.disk, Disk::Memory(_)) {
1928                    panic!("already specified a backing vmgs file");
1929                }
1930                vmgs.disk = disk;
1931            }
1932            PetriVmgsResource::Ephemeral => {
1933                panic!("attempted to specify a backing vmgs with ephemeral guest state")
1934            }
1935        }
1936        self
1937    }
1938
1939    /// Set the boot device type for the VM.
1940    ///
1941    /// This overrides the default, which is determined by the firmware type.
1942    pub fn with_boot_device_type(mut self, boot: BootDeviceType) -> Self {
1943        self.boot_device_type = boot;
1944        self
1945    }
1946
1947    /// Override the PCIe root port used by [`BootDeviceType::PcieNvme`] or
1948    /// [`BootDeviceType::PcieVirtioBlk`].
1949    ///
1950    /// The named port must exist in the PCIe topology added via the backend's
1951    /// `with_pcie_root_topology`. Defaults to `s0rc0rp0`.
1952    pub fn with_pcie_boot_port(mut self, port_name: &str) -> Self {
1953        self.pcie_boot_port = Some(port_name.to_string());
1954        self
1955    }
1956
1957    /// Enable the TPM for the VM.
1958    pub fn with_tpm(mut self, enable: bool) -> Self {
1959        if enable {
1960            self.config.tpm.get_or_insert_default();
1961        } else {
1962            self.config.tpm = None;
1963        }
1964        self
1965    }
1966
1967    /// Enable or disable the TPM state persistence for the VM.
1968    pub fn with_tpm_state_persistence(mut self, tpm_state_persistence: bool) -> Self {
1969        self.config
1970            .tpm
1971            .as_mut()
1972            .expect("TPM persistence requires a TPM")
1973            .no_persistent_secrets = !tpm_state_persistence;
1974        self
1975    }
1976
1977    /// Set the hardware sealing policy for the VM's TPM.
1978    pub fn with_hardware_sealing_policy(mut self, policy: PetriHardwareSealingPolicy) -> Self {
1979        self.config
1980            .tpm
1981            .as_mut()
1982            .expect("hardware sealing policy requires a TPM")
1983            .hardware_sealing_policy = policy;
1984        self
1985    }
1986
1987    /// Select which TPM reference implementation version the VM's TPM runs.
1988    pub fn with_tpm_version(mut self, version: PetriTpmVersion) -> Self {
1989        self.config
1990            .tpm
1991            .as_mut()
1992            .expect("TPM version requires a TPM")
1993            .version = version;
1994        self
1995    }
1996
1997    /// Add custom VTL 2 settings.
1998    // TODO: At some point we want to replace uses of this with nicer with_disk,
1999    // with_nic, etc. methods.
2000    pub fn with_custom_vtl2_settings(
2001        mut self,
2002        f: impl FnOnce(&mut Vtl2Settings) + 'static + Send + Sync,
2003    ) -> Self {
2004        f(self
2005            .config
2006            .firmware
2007            .vtl2_settings()
2008            .expect("Custom VTL 2 settings are only supported with OpenHCL"));
2009        self
2010    }
2011
2012    /// Add a storage controller to VTL2
2013    pub fn add_vtl2_storage_controller(self, controller: StorageController) -> Self {
2014        self.with_custom_vtl2_settings(move |v| {
2015            v.dynamic
2016                .as_mut()
2017                .unwrap()
2018                .storage_controllers
2019                .push(controller)
2020        })
2021    }
2022
2023    /// Add an additional SCSI controller to the VM.
2024    pub fn add_vmbus_storage_controller(
2025        mut self,
2026        id: &Guid,
2027        target_vtl: Vtl,
2028        controller_type: VmbusStorageType,
2029    ) -> Self {
2030        if self
2031            .config
2032            .vmbus_storage_controllers
2033            .insert(
2034                *id,
2035                VmbusStorageController::new(target_vtl, controller_type),
2036            )
2037            .is_some()
2038        {
2039            panic!("storage controller {id} already existed");
2040        }
2041        self
2042    }
2043
2044    /// Add a VMBus disk drive to the VM
2045    pub fn add_vmbus_drive(
2046        mut self,
2047        drive: Drive,
2048        controller_id: &Guid,
2049        controller_location: Option<u32>,
2050    ) -> Self {
2051        let controller = self
2052            .config
2053            .vmbus_storage_controllers
2054            .get_mut(controller_id)
2055            .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
2056
2057        _ = controller.set_drive(controller_location, drive, false);
2058
2059        self
2060    }
2061
2062    /// Add a VMBus disk drive to the VM
2063    pub fn add_ide_drive(
2064        mut self,
2065        drive: Drive,
2066        controller_number: u32,
2067        controller_location: u8,
2068    ) -> Self {
2069        self.config
2070            .firmware
2071            .ide_controllers_mut()
2072            .expect("Host IDE requires PCAT with no HCL")[controller_number as usize]
2073            [controller_location as usize] = Some(drive);
2074
2075        self
2076    }
2077
2078    /// Add a physical NVMe device to the VM
2079    pub fn add_physical_nvme_device(mut self, vsid: Guid, device: PhysicalNvmeDevice) -> Self {
2080        if self
2081            .config
2082            .physical_nvme_devices
2083            .insert(vsid, device)
2084            .is_some()
2085        {
2086            panic!("physical NVMe device {vsid} already existed");
2087        }
2088        self
2089    }
2090
2091    /// Get VM's guest OS flavor
2092    pub fn os_flavor(&self) -> OsFlavor {
2093        self.config.firmware.os_flavor()
2094    }
2095
2096    /// Get whether the VM will use OpenHCL
2097    pub fn is_openhcl(&self) -> bool {
2098        self.config.firmware.is_openhcl()
2099    }
2100
2101    /// Get the isolation type of the VM
2102    pub fn isolation(&self) -> Option<IsolationType> {
2103        self.config.firmware.isolation()
2104    }
2105
2106    /// Get the machine architecture
2107    pub fn arch(&self) -> MachineArch {
2108        self.config.arch
2109    }
2110
2111    /// Get the log source for creating additional log files.
2112    pub fn log_source(&self) -> &PetriLogSource {
2113        &self.resources.log_source
2114    }
2115
2116    /// Get the default OpenHCL servicing flags for this config
2117    pub fn default_servicing_flags(&self) -> OpenHclServicingFlags {
2118        T::default_servicing_flags()
2119    }
2120
2121    /// Apply a backend-specific modification in registration order.
2122    /// May be called multiple times, including around platform presets.
2123    pub fn modify_backend(
2124        mut self,
2125        f: impl FnOnce(T::VmmConfig) -> T::VmmConfig + 'static + Send,
2126    ) -> Self {
2127        let previous = self.modify_vmm_config.take();
2128        self.modify_vmm_config = Some(ModifyFn(Box::new(move |config| {
2129            let config = match previous {
2130                Some(previous) => previous.0(config),
2131                None => config,
2132            };
2133            f(config)
2134        })));
2135        self
2136    }
2137
2138    fn add_nested_artifacts_to_agent_disk(mut self) -> Self {
2139        let Some(nested_test) = self.nested_test.as_ref() else {
2140            return self;
2141        };
2142
2143        // if we can just the files directly, make sure the nested test is under
2144        // the test content dir.
2145        if T::supports_file_sharing(&self.config.firmware) {
2146            self.config.guest_shares.insert(
2147                NESTED_CONTENT_TAG.into(),
2148                GuestShare {
2149                    host_path: PathBuf::from(
2150                        std::env::var(VMM_TESTS_CONTENT_DIR).expect("test content dir not set"),
2151                    ),
2152                    guest_path: self.agent_disk_path(&[NESTED_TEST_CONTENT_DIR.into()]),
2153                },
2154            );
2155            self.config.guest_shares.insert(
2156                NESTED_IMAGES_TAG.into(),
2157                GuestShare {
2158                    host_path: PathBuf::from(
2159                        std::env::var(VMM_TEST_IMAGES).expect("test images dir not set"),
2160                    ),
2161                    guest_path: self.agent_disk_path(&[NESTED_IMAGES_DIR.into()]),
2162                },
2163            );
2164
2165            return self;
2166        }
2167
2168        let mut extras = Vec::new();
2169
2170        let path_builder = FatFsPathBuilder(self.os_flavor());
2171
2172        extras.push((NESTED_TEST_CONTENT_DIR.to_string(), ImageEntry::Dir));
2173        extras.push((
2174            path_builder
2175                .build_guest_path(&[NESTED_TEST_CONTENT_DIR.into(), NESTED_RESULTS_DIR.into()]),
2176            ImageEntry::Dir,
2177        ));
2178        extras.push((
2179            path_builder.build_guest_path(&[NESTED_IMAGES_DIR.into()]),
2180            ImageEntry::Dir,
2181        ));
2182        extras.push((
2183            path_builder.build_guest_path(&[
2184                NESTED_TEST_CONTENT_DIR.into(),
2185                path_builder.binary_with_extension(NESTED_TEST_BINARY),
2186            ]),
2187            ImageEntry::File(nested_test.test_binary.to_path_buf()),
2188        ));
2189
2190        for (handle, src) in &nested_test.artifacts {
2191            let relative_path = path_builder.convert_guest_path(&handle.relative_path());
2192            let dest = if handle.is_image() {
2193                path_builder.build_guest_path(&[NESTED_IMAGES_DIR.into(), relative_path])
2194            } else {
2195                path_builder.build_guest_path(&[NESTED_TEST_CONTENT_DIR.into(), relative_path])
2196            };
2197
2198            for parent in path_builder.parents(&dest) {
2199                if !extras.iter().any(|(p, _)| p == parent) {
2200                    extras.push((parent.to_string(), ImageEntry::Dir))
2201                }
2202            }
2203
2204            extras.push((dest, ImageEntry::File(src.clone())));
2205        }
2206
2207        let image = self.agent_image.as_mut().expect("no guest pipette");
2208        image.add_extras(extras);
2209        // Allocate an extra 150 MB for logs and crash dumps
2210        image.allocate_free_space(150 * 1024 * 1024);
2211
2212        self
2213    }
2214
2215    /// Request nested virtualization support from the host hypervisor.
2216    pub fn with_nested_virt(mut self) -> Self {
2217        self.config.nested_virt_enabled = true;
2218        self
2219    }
2220
2221    /// Use a non-standard kernel for Linux direct tests
2222    pub fn with_custom_linux_kernel(
2223        mut self,
2224        custom_kernel: ResolvedArtifact<impl IsLoadable>,
2225    ) -> Self {
2226        let Firmware::LinuxDirect { kernel, .. } = &mut self.config.firmware else {
2227            panic!("VM is not linux direct");
2228        };
2229        *kernel = custom_kernel.erase();
2230        self
2231    }
2232}
2233
2234impl<T: PetriVmmBackend> PetriVm<T> {
2235    /// Immediately tear down the VM.
2236    pub async fn teardown(mut self) -> anyhow::Result<()> {
2237        tracing::info!("Tearing down VM...");
2238        self.runtime.take_for_teardown().teardown().await?;
2239        Ok(())
2240    }
2241
2242    /// Wait for the VM to halt, returning the reason for the halt.
2243    pub async fn wait_for_halt(&mut self) -> anyhow::Result<PetriHaltReasonDetail> {
2244        tracing::info!("Waiting for VM to halt...");
2245        let halt_reason = self.runtime.wait_for_halt(false).await?;
2246        tracing::info!("VM halted: {halt_reason:?}. Cancelling watchdogs...");
2247        futures::future::join_all(self.watchdog_tasks.drain(..).map(|t| t.cancel())).await;
2248        Ok(halt_reason)
2249    }
2250
2251    /// Wait for the VM to cleanly shutdown.
2252    pub async fn wait_for_clean_shutdown(&mut self) -> anyhow::Result<()> {
2253        let halt_reason = self.wait_for_halt().await?;
2254        if halt_reason.reason != PetriHaltReason::PowerOff {
2255            anyhow::bail!("Expected PowerOff, got {halt_reason:?}");
2256        }
2257        tracing::info!("VM was cleanly powered off and torn down.");
2258        Ok(())
2259    }
2260
2261    /// Wait for the VM to halt, returning the reason for the halt,
2262    /// and tear down the VM.
2263    pub async fn wait_for_teardown(mut self) -> anyhow::Result<PetriHaltReasonDetail> {
2264        let halt_reason = self.wait_for_halt().await?;
2265        self.teardown().await?;
2266        Ok(halt_reason)
2267    }
2268
2269    /// Wait for the VM to cleanly shutdown and tear down the VM.
2270    pub async fn wait_for_clean_teardown(mut self) -> anyhow::Result<()> {
2271        self.wait_for_clean_shutdown().await?;
2272        self.teardown().await
2273    }
2274
2275    /// Wait for the VM to reset. Does not wait for pipette.
2276    pub async fn wait_for_reset_no_agent(&mut self) -> anyhow::Result<()> {
2277        self.wait_for_reset_core().await?;
2278        self.wait_for_expected_boot_event().await?;
2279        Ok(())
2280    }
2281
2282    /// Wait for the VM to reset and pipette to connect.
2283    pub async fn wait_for_reset(&mut self) -> anyhow::Result<PipetteClient> {
2284        self.wait_for_reset_no_agent().await?;
2285        self.wait_for_agent().await
2286    }
2287
2288    async fn wait_for_reset_core(&mut self) -> anyhow::Result<()> {
2289        tracing::info!("Waiting for VM to reset...");
2290        let halt_reason = self.runtime.wait_for_halt(true).await?;
2291        if halt_reason.reason != PetriHaltReason::Reset {
2292            anyhow::bail!("Expected reset, got {halt_reason:?}");
2293        }
2294        tracing::info!("VM reset.");
2295        Ok(())
2296    }
2297
2298    /// Invoke Inspect on the running OpenHCL instance.
2299    ///
2300    /// IMPORTANT: As mentioned in the Guide, inspect output is *not* guaranteed
2301    /// to be stable. Use this to test that components in OpenHCL are working as
2302    /// you would expect. But, if you are adding a test simply to verify that
2303    /// the inspect output as some other tool depends on it, then that is
2304    /// incorrect.
2305    ///
2306    /// - `timeout` is enforced on the client side
2307    /// - `path` and `depth` are passed to the [`inspect::Inspect`] machinery.
2308    pub async fn inspect_openhcl(
2309        &self,
2310        path: impl Into<String>,
2311        depth: Option<usize>,
2312        timeout: Option<Duration>,
2313    ) -> anyhow::Result<inspect::Node> {
2314        self.openhcl_diag()?
2315            .inspect(path.into().as_str(), depth, timeout)
2316            .await
2317    }
2318
2319    /// Invoke Update (Inspect protocol) on the running OpenHCL instance.
2320    ///
2321    /// IMPORTANT: As mentioned in the Guide, inspect output is *not* guaranteed
2322    /// to be stable. Use this to test that components in OpenHCL are working as
2323    /// you would expect. But, if you are adding a test simply to verify that
2324    /// the inspect output as some other tool depends on it, then that is
2325    /// incorrect.
2326    ///
2327    /// - `path` and `value` are passed to the [`inspect::Inspect`] machinery.
2328    pub async fn inspect_update_openhcl(
2329        &self,
2330        path: impl Into<String>,
2331        value: impl Into<String>,
2332    ) -> anyhow::Result<inspect::Value> {
2333        self.openhcl_diag()?
2334            .inspect_update(path.into(), value.into())
2335            .await
2336    }
2337
2338    /// Test that we are able to inspect OpenHCL.
2339    pub async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()> {
2340        self.inspect_openhcl("", None, None).await.map(|_| ())
2341    }
2342
2343    /// Dump OpenHCL's saved state through the diagnostic interface.
2344    ///
2345    /// A running VM is briefly paused to capture its state, then resumed.
2346    pub async fn dump_openhcl_saved_state(&self) -> anyhow::Result<Vec<u8>> {
2347        self.openhcl_diag()?.dump_saved_state().await
2348    }
2349
2350    /// Invoke Inspect on the running VMM process itself (e.g. OpenVMM),
2351    /// returning the inspect tree rooted at `path` (pass `""` for the whole
2352    /// tree).
2353    ///
2354    /// Only backends that expose an inspect interface (currently OpenVMM)
2355    /// support this; other backends return an error.
2356    ///
2357    /// IMPORTANT: As mentioned in the Guide, inspect output is *not* guaranteed
2358    /// to be stable. Use this to verify that components are working as you
2359    /// expect, not to assert on output that some other tool depends on.
2360    pub async fn inspect_vmm(&self, path: &str) -> anyhow::Result<inspect::Node> {
2361        use anyhow::Context;
2362
2363        let inspector = self
2364            .runtime
2365            .inspector()
2366            .context("this VMM backend does not support inspect")?;
2367        inspector.inspect(path).await
2368    }
2369
2370    /// Wait for VTL 2 to report that it is ready to respond to commands.
2371    /// Will fail if the VM is not running OpenHCL.
2372    ///
2373    /// This should only be necessary if you're doing something manual. All
2374    /// Petri-provided methods will wait for VTL 2 to be ready automatically.
2375    pub async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()> {
2376        self.openhcl_diag()?.wait_for_vtl2().await
2377    }
2378
2379    /// Get the kmsg stream from OpenHCL.
2380    pub async fn kmsg(&self) -> anyhow::Result<diag_client::kmsg_stream::KmsgStream> {
2381        self.openhcl_diag()?.kmsg().await
2382    }
2383
2384    /// Gets a live core dump of the OpenHCL process specified by 'name' and
2385    /// writes it to 'path'
2386    pub async fn openhcl_core_dump(&self, name: &str, path: &Path) -> anyhow::Result<()> {
2387        self.openhcl_diag()?.core_dump(name, path).await
2388    }
2389
2390    /// Crashes the specified openhcl process
2391    pub async fn openhcl_crash(&self, name: &str) -> anyhow::Result<()> {
2392        self.openhcl_diag()?.crash(name).await
2393    }
2394
2395    /// Wait for a connection from a pipette agent running in the guest.
2396    /// Useful if you've rebooted the vm or are otherwise expecting a fresh connection.
2397    async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient> {
2398        // As a workaround for #2470 (where the guest crashes when the pipette
2399        // connection timeout expires due to a vmbus bug), wait for the shutdown
2400        // IC to come online first so that we probably won't time out when
2401        // connecting to the agent.
2402        // TODO: remove this once the bug is fixed, since it shouldn't be
2403        // necessary and a guest could in theory support pipette and not the IC
2404        //
2405        // This is a no-op when the shutdown IC is not configured (e.g.,
2406        // no VMBus or minimal mode).
2407        self.runtime.wait_for_enlightened_shutdown_ready().await?;
2408        let client = self.runtime.wait_for_agent(false).await?;
2409        self.mount_guest_shares(&client).await?;
2410        Ok(client)
2411    }
2412
2413    async fn mount_guest_shares(&self, client: &PipetteClient) -> anyhow::Result<()> {
2414        if !self.guest_shares.is_empty() {
2415            anyhow::ensure!(
2416                self.properties.os_flavor == OsFlavor::Linux,
2417                "directory sharing requires a Linux guest"
2418            );
2419            let mut shares = self.guest_shares.iter().collect::<Vec<_>>();
2420            // Mount parents first so they do not hide descendant shares.
2421            shares.sort_by_key(|(tag, share)| {
2422                (
2423                    share
2424                        .guest_path
2425                        .split('/')
2426                        .filter(|part| !part.is_empty() && *part != ".")
2427                        .count(),
2428                    *tag,
2429                )
2430            });
2431            for (tag, share) in shares {
2432                let shell = client.unix_shell();
2433                shell
2434                    .cmd("mkdir")
2435                    .args(["-p", &share.guest_path])
2436                    .run()
2437                    .await?;
2438                shell
2439                    .cmd("mount")
2440                    .args([
2441                        "-t",
2442                        "9p",
2443                        "-o",
2444                        "trans=virtio,version=9p2000.L",
2445                        tag,
2446                        &share.guest_path,
2447                    ])
2448                    .run()
2449                    .await
2450                    .with_context(|| format!("mounting guest share {tag}"))?;
2451            }
2452        }
2453        Ok(())
2454    }
2455
2456    /// Wait for a connection from a pipette agent running in VTL 2.
2457    /// Useful if you've reset VTL 2 or are otherwise expecting a fresh connection.
2458    /// Will fail if the VM is not running OpenHCL.
2459    pub async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient> {
2460        // VTL 2's pipette doesn't auto launch, only launch it on demand
2461        self.launch_vtl2_pipette().await?;
2462        self.runtime.wait_for_agent(true).await
2463    }
2464
2465    /// Waits for an event emitted by the firmware about its boot status, and
2466    /// verifies that it is the expected success value.
2467    ///
2468    /// * Linux Direct guests do not emit a boot event, so this method immediately returns Ok.
2469    /// * PCAT guests may not emit an event depending on the PCAT version, this
2470    ///   method is best effort for them.
2471    async fn wait_for_expected_boot_event(&mut self) -> anyhow::Result<()> {
2472        if let Some(expected_event) = self.expected_boot_event {
2473            let event = self.wait_for_boot_event().await?;
2474
2475            anyhow::ensure!(
2476                event == expected_event,
2477                "Did not receive expected boot event"
2478            );
2479        } else {
2480            tracing::warn!("Boot event not emitted for configured firmware or manually ignored.");
2481        }
2482
2483        Ok(())
2484    }
2485
2486    /// Waits for an event emitted by the firmware about its boot status, and
2487    /// returns that status.
2488    async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
2489        tracing::info!("Waiting for boot event...");
2490        let boot_event = loop {
2491            if let Some(event) = self
2492                .runtime
2493                .wait_for_boot_event(self.vmm_quirks.flaky_boot)
2494                .await?
2495            {
2496                break event;
2497            }
2498
2499            tracing::error!("Did not get boot event in required time, resetting...");
2500            if let Some(inspector) = self.runtime.inspector() {
2501                collect_inspect(
2502                    "vmm",
2503                    Box::pin(async move { inspector.inspect("").await }),
2504                    &self.resources.log_source,
2505                    "timeout",
2506                )
2507                .await;
2508            }
2509
2510            self.runtime.reset().await?;
2511        };
2512        tracing::info!("Got boot event: {boot_event:?}");
2513        Ok(boot_event)
2514    }
2515
2516    /// Wait for the Hyper-V shutdown IC to be ready and use it to instruct
2517    /// the guest to shutdown.
2518    pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
2519        tracing::info!("Waiting for enlightened shutdown to be ready");
2520        self.runtime.wait_for_enlightened_shutdown_ready().await?;
2521
2522        // all guests used in testing have been observed to intermittently
2523        // drop shutdown requests if they are sent too soon after the shutdown
2524        // ic comes online. give them a little extra time.
2525        // TODO: use a different method of determining whether the VM has booted
2526        // or debug and fix the shutdown IC.
2527        let mut wait_time = Duration::from_secs(10);
2528
2529        // some guests need even more time
2530        if let Some(duration) = self.guest_quirks.hyperv_shutdown_ic_sleep {
2531            wait_time += duration;
2532        }
2533
2534        tracing::info!(
2535            "Shutdown IC reported ready, waiting for an extra {}s",
2536            wait_time.as_secs()
2537        );
2538        PolledTimer::new(&self.resources.driver)
2539            .sleep(wait_time)
2540            .await;
2541
2542        tracing::info!("Sending enlightened shutdown command");
2543        self.runtime.send_enlightened_shutdown(kind).await
2544    }
2545
2546    /// Instruct the OpenHCL to restart the VTL2 paravisor. Will fail if the VM
2547    /// is not running OpenHCL. Will also fail if the VM is not running.
2548    pub async fn restart_openhcl(
2549        &mut self,
2550        new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
2551        flags: OpenHclServicingFlags,
2552    ) -> anyhow::Result<()> {
2553        self.runtime
2554            .restart_openhcl(&new_openhcl.erase(), flags)
2555            .await
2556    }
2557
2558    /// Update the command line parameter of the running VM that will apply on next boot.
2559    /// Will fail if the VM is not using IGVM load mode.
2560    pub async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()> {
2561        self.runtime.update_command_line(command_line).await
2562    }
2563
2564    /// Hot-add a PCIe device to a named port at runtime.
2565    pub async fn add_pcie_device(
2566        &mut self,
2567        port_name: String,
2568        resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
2569    ) -> anyhow::Result<()> {
2570        self.runtime.add_pcie_device(port_name, resource).await
2571    }
2572
2573    /// Hot-remove a PCIe device from a named port at runtime.
2574    pub async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
2575        self.runtime.remove_pcie_device(port_name).await
2576    }
2577
2578    /// Instruct the OpenHCL to save the state of the VTL2 paravisor. Will fail if the VM
2579    /// is not running OpenHCL. Will also fail if the VM is not running or if this is called twice in succession
2580    pub async fn save_openhcl(
2581        &mut self,
2582        new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
2583        flags: OpenHclServicingFlags,
2584    ) -> anyhow::Result<()> {
2585        self.runtime.save_openhcl(&new_openhcl.erase(), flags).await
2586    }
2587
2588    /// Instruct the OpenHCL to restore the state of the VTL2 paravisor. Will fail if the VM
2589    /// is not running OpenHCL. Will also fail if the VM is running or if this is called without prior save
2590    pub async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
2591        self.runtime.restore_openhcl().await
2592    }
2593
2594    /// Get VM's guest OS flavor
2595    pub fn arch(&self) -> MachineArch {
2596        self.arch
2597    }
2598
2599    /// Get the inner runtime backend to make backend-specific calls
2600    pub fn backend(&mut self) -> &mut T::VmRuntime {
2601        &mut self.runtime
2602    }
2603
2604    async fn launch_vtl2_pipette(&self) -> anyhow::Result<()> {
2605        tracing::debug!("Launching VTL 2 pipette...");
2606
2607        // Start pipette through DiagClient
2608        let res = self
2609            .openhcl_diag()?
2610            .run_vtl2_command("sh", &["-c", "mkdir /cidata && mount LABEL=cidata /cidata"])
2611            .await?;
2612
2613        if !res.exit_status.success() {
2614            anyhow::bail!("Failed to mount VTL 2 pipette drive: {:?}", res);
2615        }
2616
2617        let res = self
2618            .openhcl_diag()?
2619            .run_detached_vtl2_command("sh", &["-c", "/cidata/pipette 2>&1 | logger &"])
2620            .await?;
2621
2622        if !res.success() {
2623            anyhow::bail!("Failed to spawn VTL 2 pipette: {:?}", res);
2624        }
2625
2626        Ok(())
2627    }
2628
2629    fn openhcl_diag(&self) -> anyhow::Result<&OpenHclDiagHandler> {
2630        if let Some(ohd) = self.openhcl_diag_handler.as_ref() {
2631            Ok(ohd)
2632        } else {
2633            anyhow::bail!("VM is not configured with OpenHCL")
2634        }
2635    }
2636
2637    /// Get the path to the VM's guest state file
2638    pub async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2639        self.runtime.get_guest_state_file().await
2640    }
2641
2642    /// Modify OpenHCL VTL2 settings.
2643    pub async fn modify_vtl2_settings(
2644        &mut self,
2645        f: impl FnOnce(&mut Vtl2Settings),
2646    ) -> anyhow::Result<()> {
2647        if self.openhcl_diag_handler.is_none() {
2648            panic!("Custom VTL 2 settings are only supported with OpenHCL");
2649        }
2650        f(self
2651            .config
2652            .vtl2_settings
2653            .get_or_insert_with(default_vtl2_settings));
2654        self.runtime
2655            .set_vtl2_settings(self.config.vtl2_settings.as_ref().unwrap())
2656            .await
2657    }
2658
2659    /// Get the list of storage controllers added to this VM
2660    pub fn get_vmbus_storage_controllers(&self) -> &HashMap<Guid, VmbusStorageController> {
2661        &self.config.vmbus_storage_controllers
2662    }
2663
2664    /// Add or modify a VMBus disk drive
2665    pub async fn set_vmbus_drive(
2666        &mut self,
2667        drive: Drive,
2668        controller_id: &Guid,
2669        controller_location: Option<u32>,
2670    ) -> anyhow::Result<()> {
2671        let controller = self
2672            .config
2673            .vmbus_storage_controllers
2674            .get_mut(controller_id)
2675            .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
2676
2677        let controller_location = controller.set_drive(controller_location, drive, true);
2678        let disk = controller.drives.get(&controller_location).unwrap();
2679
2680        self.runtime
2681            .set_vmbus_drive(disk, controller_id, controller_location)
2682            .await?;
2683
2684        Ok(())
2685    }
2686
2687    /// Run the nested test. Failed if a nested test was not configured.
2688    pub async fn run_nested_test(
2689        &self,
2690        client: &PipetteClient,
2691        env: BTreeMap<String, String>,
2692    ) -> anyhow::Result<()> {
2693        let nested_test = self.nested_test.as_ref().context("no nested test")?;
2694
2695        let test_binary_path =
2696            if let Some(test_content_dir) = self.guest_shares.get(NESTED_CONTENT_TAG) {
2697                let test_content_dir = absolute(&test_content_dir.host_path)?;
2698                let local_test_binary_path = absolute(&nested_test.test_binary)?;
2699
2700                self.convert_guest_path(
2701                    local_test_binary_path
2702                        .strip_prefix(&test_content_dir)
2703                        .context("test binary should be under content dir")?,
2704                )
2705            } else {
2706                self.binary_with_extension(NESTED_TEST_BINARY)
2707            };
2708
2709        let mut cmd = client
2710            .command(self.agent_disk_path(&[NESTED_TEST_CONTENT_DIR.into(), test_binary_path]));
2711        cmd.arg("--ignored")
2712            .arg("--exact")
2713            .arg(&nested_test.test_name)
2714            .env(
2715                VMM_TESTS_CONTENT_DIR,
2716                self.agent_disk_path(&[NESTED_TEST_CONTENT_DIR.into()]),
2717            )
2718            .env(
2719                VMM_TEST_IMAGES,
2720                self.agent_disk_path(&[NESTED_IMAGES_DIR.into()]),
2721            )
2722            .env(
2723                TEST_OUTPUT_PATH,
2724                self.agent_disk_path(&[NESTED_TEST_CONTENT_DIR.into(), NESTED_RESULTS_DIR.into()]),
2725            )
2726            .stdout(crate::pipette::process::Stdio::piped())
2727            .stderr(crate::pipette::process::Stdio::piped());
2728
2729        for (k, v) in env {
2730            cmd.env(k, v);
2731        }
2732
2733        if self.guest_shares.contains_key(NESTED_CONTENT_TAG) {
2734            cmd.env(PETRI_NESTED_TEST_PARENT, &self.name);
2735        }
2736
2737        if let Ok(v) = std::env::var(PETRI_REMOTE_ARTIFACTS) {
2738            cmd.env(PETRI_REMOTE_ARTIFACTS, v);
2739        }
2740
2741        let mut child = cmd.spawn().await?;
2742
2743        // TODO: output JSON, parse, and correctly format in logs
2744        self.resources
2745            .driver
2746            .spawn(
2747                "log_nested",
2748                crate::log_task(
2749                    self.resources.log_source.log_file("stdout")?,
2750                    child.stdout.take().unwrap(),
2751                    "nested stdout",
2752                ),
2753            )
2754            .detach();
2755
2756        let output = child.wait_with_output().await?;
2757
2758        // copy the files if we aren't sharing the results folder directly
2759        if !self.guest_shares.contains_key(NESTED_CONTENT_TAG) {
2760            let test_results_dir = self.agent_disk_path(&[
2761                NESTED_TEST_CONTENT_DIR.into(),
2762                NESTED_RESULTS_DIR.into(),
2763                nested_test.test_name.replace("::", "__"),
2764            ]);
2765
2766            for file in client.list_dir(&test_results_dir).await? {
2767                let data = client
2768                    .read_file(self.build_guest_path(&[test_results_dir.clone(), file.clone()]))
2769                    .await?;
2770                let mut output = self
2771                    .resources
2772                    .log_source
2773                    .create_attachment_no_prefix(&file)?;
2774                output.write_all(&data)?;
2775            }
2776        }
2777
2778        if !output.status.success() {
2779            anyhow::bail!(
2780                "nested test exited with status {}: {}",
2781                output.status,
2782                String::from_utf8_lossy(&output.stderr)
2783            );
2784        }
2785
2786        Ok(())
2787    }
2788}
2789
2790/// A running VM that tests can interact with.
2791#[async_trait]
2792pub trait PetriVmRuntime: Send + Sync + 'static {
2793    /// Interface for inspecting the VM
2794    type VmInspector: PetriVmInspector;
2795    /// Interface for accessing the framebuffer
2796    type VmFramebufferAccess: PetriVmFramebufferAccess;
2797
2798    /// Cleanly tear down the VM immediately.
2799    async fn teardown(self) -> anyhow::Result<()>;
2800    /// Wait for the VM to halt, returning the reason for the halt. The VM
2801    /// should automatically restart the VM on reset if `allow_reset` is true.
2802    async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReasonDetail>;
2803    /// Wait for a connection from a pipette agent
2804    async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>;
2805    /// Get an OpenHCL diagnostics handler for the VM
2806    fn openhcl_diag(&self) -> Option<OpenHclDiagHandler>;
2807    /// Waits for an event emitted by the firmware about its boot status, and
2808    /// returns that status. Returns `None` if `timeout` elapsed first.
2809    async fn wait_for_boot_event(
2810        &mut self,
2811        timeout: Option<Duration>,
2812    ) -> anyhow::Result<Option<FirmwareEvent>>;
2813    /// Waits for the Hyper-V shutdown IC to be ready
2814    // TODO: return a receiver that will be closed when it is no longer ready.
2815    async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()>;
2816    /// Instruct the guest to shutdown via the Hyper-V shutdown IC.
2817    async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
2818    /// Instruct the OpenHCL to restart the VTL2 paravisor. Will fail if the VM
2819    /// is not running OpenHCL. Will also fail if the VM is not running.
2820    async fn restart_openhcl(
2821        &mut self,
2822        new_openhcl: &ResolvedArtifact,
2823        flags: OpenHclServicingFlags,
2824    ) -> anyhow::Result<()>;
2825    /// Instruct the OpenHCL to save the state of the VTL2 paravisor. Will fail if the VM
2826    /// is not running OpenHCL. Will also fail if the VM is not running or if this is called twice in succession
2827    /// without a call to `restore_openhcl`.
2828    async fn save_openhcl(
2829        &mut self,
2830        new_openhcl: &ResolvedArtifact,
2831        flags: OpenHclServicingFlags,
2832    ) -> anyhow::Result<()>;
2833    /// Instruct the OpenHCL to restore the state of the VTL2 paravisor. Will fail if the VM
2834    /// is not running OpenHCL. Will also fail if the VM is running or if this is called without prior save.
2835    async fn restore_openhcl(&mut self) -> anyhow::Result<()>;
2836    /// Update the command line parameter of the running VM that will apply on next boot.
2837    /// Will fail if the VM is not using IGVM load mode.
2838    async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()>;
2839    /// If the backend supports it, get an inspect interface
2840    fn inspector(&self) -> Option<Self::VmInspector> {
2841        None
2842    }
2843    /// If the backend supports it, take the screenshot interface
2844    /// (subsequent calls may return None).
2845    fn take_framebuffer_access(&mut self) -> Option<Self::VmFramebufferAccess> {
2846        None
2847    }
2848    /// Issue a hard reset to the VM
2849    async fn reset(&mut self) -> anyhow::Result<()>;
2850    /// Get the path to the VM's guest state file
2851    async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2852        Ok(None)
2853    }
2854    /// Set the OpenHCL VTL2 settings
2855    async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()>;
2856    /// Add or modify a VMBus disk drive
2857    async fn set_vmbus_drive(
2858        &mut self,
2859        disk: &Drive,
2860        controller_id: &Guid,
2861        controller_location: u32,
2862    ) -> anyhow::Result<()>;
2863    /// Hot-add a PCIe device to a named port at runtime.
2864    async fn add_pcie_device(
2865        &mut self,
2866        port_name: String,
2867        resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
2868    ) -> anyhow::Result<()> {
2869        let _ = (port_name, resource);
2870        anyhow::bail!("PCIe hotplug not supported by this backend")
2871    }
2872    /// Hot-remove a PCIe device from a named port at runtime.
2873    async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
2874        let _ = port_name;
2875        anyhow::bail!("PCIe hotplug not supported by this backend")
2876    }
2877}
2878
2879/// Interface for getting information about the state of the VM
2880#[async_trait]
2881pub trait PetriVmInspector: Send + Sync + 'static {
2882    /// Get information about the state of the VM at the given inspect `path`.
2883    /// Pass `""` to inspect the entire tree.
2884    async fn inspect(&self, path: &str) -> anyhow::Result<inspect::Node>;
2885}
2886
2887/// Use this for the associated type if not supported
2888pub struct NoPetriVmInspector;
2889#[async_trait]
2890impl PetriVmInspector for NoPetriVmInspector {
2891    async fn inspect(&self, _path: &str) -> anyhow::Result<inspect::Node> {
2892        unreachable!()
2893    }
2894}
2895
2896/// Raw VM screenshot
2897pub struct VmScreenshotMeta {
2898    /// color encoding used by the image
2899    pub color: image::ExtendedColorType,
2900    /// x dimension
2901    pub width: u16,
2902    /// y dimension
2903    pub height: u16,
2904}
2905
2906/// Interface for getting screenshots of the VM
2907#[async_trait]
2908pub trait PetriVmFramebufferAccess: Send + 'static {
2909    /// Populates the provided buffer with a screenshot of the VM,
2910    /// returning the dimensions and color type.
2911    async fn screenshot(&mut self, image: &mut Vec<u8>)
2912    -> anyhow::Result<Option<VmScreenshotMeta>>;
2913}
2914
2915/// Use this for the associated type if not supported
2916pub struct NoPetriVmFramebufferAccess;
2917#[async_trait]
2918impl PetriVmFramebufferAccess for NoPetriVmFramebufferAccess {
2919    async fn screenshot(
2920        &mut self,
2921        _image: &mut Vec<u8>,
2922    ) -> anyhow::Result<Option<VmScreenshotMeta>> {
2923        unreachable!()
2924    }
2925}
2926
2927/// Common processor topology information for the VM.
2928#[derive(Debug, Clone)]
2929pub struct ProcessorTopology {
2930    /// The number of virtual processors.
2931    pub vp_count: u32,
2932    /// Whether SMT (hyperthreading) is enabled.
2933    pub enable_smt: Option<bool>,
2934    /// The number of virtual processors per socket.
2935    pub vps_per_socket: Option<u32>,
2936    /// The APIC configuration (x86-64 only).
2937    pub apic_mode: Option<ApicMode>,
2938}
2939
2940impl Default for ProcessorTopology {
2941    fn default() -> Self {
2942        Self {
2943            vp_count: 2,
2944            enable_smt: None,
2945            vps_per_socket: None,
2946            apic_mode: None,
2947        }
2948    }
2949}
2950
2951impl ProcessorTopology {
2952    /// A large number of VPs
2953    pub fn heavy() -> Self {
2954        Self {
2955            vp_count: 16,
2956            vps_per_socket: Some(8),
2957            ..Default::default()
2958        }
2959    }
2960
2961    /// A very large number of VPs
2962    pub fn very_heavy() -> Self {
2963        Self {
2964            vp_count: 32,
2965            vps_per_socket: Some(16),
2966            ..Default::default()
2967        }
2968    }
2969}
2970
2971/// The APIC mode for the VM.
2972#[derive(Debug, Clone, Copy)]
2973pub enum ApicMode {
2974    /// xAPIC mode only.
2975    Xapic,
2976    /// x2APIC mode supported but not enabled at boot.
2977    X2apicSupported,
2978    /// x2APIC mode enabled at boot.
2979    X2apicEnabled,
2980}
2981
2982/// Common memory configuration information for the VM.
2983#[derive(Debug, Clone)]
2984pub struct MemoryConfig {
2985    /// Specifies the amount of memory, in bytes, to assign to the
2986    /// virtual machine.
2987    pub startup_bytes: u64,
2988    /// Specifies the minimum and maximum amount of dynamic memory, in bytes.
2989    ///
2990    /// Dynamic memory will be disabled if this is `None`.
2991    pub dynamic_memory_range: Option<(u64, u64)>,
2992    /// Per-NUMA-node memory sizes. When set, RAM is distributed across
2993    /// vNUMA nodes instead of assigning all RAM to node 0.
2994    pub numa_mem_sizes: Option<Vec<u64>>,
2995    /// Whether to back guest RAM with private anonymous memory rather than a
2996    /// shared (file/memfd-backed) memory section.
2997    ///
2998    /// - `None` (the default) uses private memory whenever the configuration
2999    ///   allows it, falling back to shared memory otherwise. Private anonymous
3000    ///   memory is cheaper to set up and eligible for Transparent Huge Pages,
3001    ///   so it is preferred for performance; this lets each VM get the best
3002    ///   backing for its firmware without the test having to know the details.
3003    /// - `Some(true)` explicitly requires private memory. This is an error if
3004    ///   the configuration is incompatible with private memory (OpenHCL, which
3005    ///   shares VTL0 RAM with VTL2 via a remote mapper, and PCAT/Gen1, which
3006    ///   relies on x86 legacy support, both require shared memory), rather than
3007    ///   silently downgrading to shared.
3008    /// - `Some(false)` explicitly requires shared memory, for tests that need
3009    ///   the guest RAM backing to be shareable with another process, such as
3010    ///   vhost-user backends.
3011    ///
3012    /// Only applies to the OpenVMM backend; ignored by Hyper-V.
3013    pub private_memory: Option<bool>,
3014    /// Mark guest RAM as eligible for Transparent Huge Pages (THP),
3015    /// improving performance for large allocations.
3016    ///
3017    /// Defaults to `true`. Applies to private anonymous guest RAM and to
3018    /// shared memfd-backed RAM, on Linux (via `madvise`) and on Windows (via
3019    /// soft large pages). It has no effect on explicit hugetlb/large-page
3020    /// backings (see
3021    /// [`with_hugepages`](crate::openvmm::PetriVmConfigOpenVmm::with_hugepages)),
3022    /// which are already huge.
3023    ///
3024    /// Only applies to the OpenVMM backend; ignored by Hyper-V.
3025    pub transparent_hugepages: bool,
3026}
3027
3028impl Default for MemoryConfig {
3029    fn default() -> Self {
3030        Self {
3031            startup_bytes: 4 * 1024 * 1024 * 1024, // 4 GiB
3032            dynamic_memory_range: None,
3033            numa_mem_sizes: None,
3034            private_memory: None,
3035            transparent_hugepages: true,
3036        }
3037    }
3038}
3039
3040/// UEFI firmware configuration
3041#[derive(Debug, Clone)]
3042pub struct UefiConfig {
3043    /// Enable secure boot
3044    pub secure_boot_enabled: bool,
3045    /// Secure boot template
3046    pub secure_boot_template: Option<SecureBootTemplate>,
3047    /// Custom UEFI variable delta JSON
3048    pub custom_uefi_json: Option<Vec<u8>>,
3049    /// Disable the UEFI frontpage which will cause the VM to shutdown instead when unable to boot.
3050    pub disable_frontpage: bool,
3051    /// Always attempt a default boot
3052    pub default_boot_always_attempt: bool,
3053    /// Enable vPCI boot (for NVMe)
3054    pub enable_vpci_boot: bool,
3055    /// Force UEFI to bounce-buffer all DMA traffic
3056    pub force_dma_bounce: bool,
3057    /// EFI diagnostics log level filter
3058    pub efi_diagnostics_log_level: EfiDiagnosticsLogLevel,
3059    /// Per-period rate-limit override for EFI diagnostics emission.
3060    /// See [`PetriVmBuilder::with_efi_diagnostics_rate_limit()`] for more information.
3061    pub efi_diagnostics_rate_limit: Option<u32>,
3062}
3063
3064impl Default for UefiConfig {
3065    fn default() -> Self {
3066        Self {
3067            secure_boot_enabled: false,
3068            secure_boot_template: None,
3069            custom_uefi_json: None,
3070            disable_frontpage: true,
3071            default_boot_always_attempt: false,
3072            enable_vpci_boot: false,
3073            force_dma_bounce: false,
3074            efi_diagnostics_log_level: EfiDiagnosticsLogLevel::Default,
3075            efi_diagnostics_rate_limit: None,
3076        }
3077    }
3078}
3079
3080/// EFI diagnostics log level filter.
3081///
3082/// Controls which UEFI diagnostics log entries are forwarded to the host
3083/// tracing infrastructure (and thus visible via kmsg / test output).
3084#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
3085pub enum EfiDiagnosticsLogLevel {
3086    /// Default log level (ERROR and WARN only).
3087    #[default]
3088    Default,
3089    /// Include INFO logs (ERROR, WARN, and INFO).
3090    Info,
3091    /// All log levels.
3092    Full,
3093}
3094
3095/// Control the logging configuration of OpenVMM/OpenHCL.
3096#[derive(Debug, Clone)]
3097pub enum OpenvmmLogConfig {
3098    /// Use the default log levels used by petri tests. This will forward
3099    /// `OPENVMM_LOG` and `OPENVMM_SHOW_SPANS` from the environment if they are
3100    /// set, otherwise it will use `debug` and `false` respectively
3101    TestDefault,
3102    /// Use the built-in default log levels of OpenHCL/OpenVMM (e.g. don't pass
3103    /// OPENVMM_LOG or OPENVMM_SHOW_SPANS)
3104    BuiltInDefault,
3105    /// Use the provided custom log levels, specified as key/value pairs. At this time,
3106    /// simply uses the already-defined environment variables (e.g.
3107    /// `OPENVMM_LOG=info,disk_nvme=debug OPENVMM_SHOW_SPANS=true`)
3108    ///
3109    /// See the Guide and source code for configuring these logs.
3110    /// - For the host VMM: see `enable_tracing` in `tracing_init.rs` for details on
3111    ///   the accepted keys and values.
3112    /// - For OpenHCL, see `init_tracing_backend` in `openhcl/src/logging/mod.rs` for details on
3113    ///   the accepted keys and values.
3114    Custom(BTreeMap<String, String>),
3115}
3116
3117/// OpenHCL configuration
3118#[derive(Debug, Clone)]
3119pub struct OpenHclConfig {
3120    /// Whether to enable VMBus redirection
3121    pub vmbus_redirect: bool,
3122    /// Whether to enable MANA keepalive at boot.
3123    pub enable_mana_keepalive: bool,
3124    /// Test-specified command-line parameters to append to the petri generated
3125    /// command line and pass to OpenHCL. VM backends should use
3126    /// [`OpenHclConfig::command_line()`] rather than reading this directly.
3127    pub custom_command_line: Option<String>,
3128    /// Command line parameters that control OpenHCL logging behavior. Separate
3129    /// from `command_line` so that petri can decide to use default log
3130    /// levels.
3131    pub log_levels: OpenvmmLogConfig,
3132    /// How to place VTL2 in address space. If `None`, the backend VMM
3133    /// will decide on default behavior.
3134    pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
3135    /// VTL2 settings
3136    pub vtl2_settings: Option<Vtl2Settings>,
3137}
3138
3139impl OpenHclConfig {
3140    /// Returns the command line to pass to OpenHCL based on these parameters. Aggregates
3141    /// the command line and log levels.
3142    pub fn command_line(&self) -> String {
3143        let mut cmdline = self.custom_command_line.clone();
3144
3145        if self.enable_mana_keepalive {
3146            append_cmdline(&mut cmdline, "OPENHCL_MANA_KEEP_ALIVE=host,privatepool");
3147        }
3148
3149        match &self.log_levels {
3150            OpenvmmLogConfig::TestDefault => {
3151                let default_log_levels = {
3152                    // Forward OPENVMM_LOG and OPENVMM_SHOW_SPANS to OpenHCL if they're set.
3153                    let openhcl_tracing = if let Ok(x) =
3154                        std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG"))
3155                    {
3156                        format!("OPENVMM_LOG={x}")
3157                    } else {
3158                        "OPENVMM_LOG=debug".to_owned()
3159                    };
3160                    let openhcl_show_spans = if let Ok(x) = std::env::var("OPENVMM_SHOW_SPANS") {
3161                        format!("OPENVMM_SHOW_SPANS={x}")
3162                    } else {
3163                        "OPENVMM_SHOW_SPANS=false".to_owned()
3164                    };
3165                    format!("{openhcl_tracing} {openhcl_show_spans}")
3166                };
3167                append_cmdline(&mut cmdline, &default_log_levels);
3168            }
3169            OpenvmmLogConfig::BuiltInDefault => {
3170                // do nothing, use whatever the built-in default is
3171            }
3172            OpenvmmLogConfig::Custom(levels) => {
3173                levels.iter().for_each(|(key, value)| {
3174                    append_cmdline(&mut cmdline, format!("{key}={value}"));
3175                });
3176            }
3177        }
3178
3179        cmdline.unwrap_or_default()
3180    }
3181}
3182
3183impl Default for OpenHclConfig {
3184    fn default() -> Self {
3185        Self {
3186            vmbus_redirect: false,
3187            enable_mana_keepalive: true,
3188            custom_command_line: None,
3189            log_levels: OpenvmmLogConfig::TestDefault,
3190            vtl2_base_address_type: None,
3191            vtl2_settings: None,
3192        }
3193    }
3194}
3195
3196/// TPM configuration
3197#[derive(Debug, Clone)]
3198pub struct TpmConfig {
3199    /// Use ephemeral TPM state (do not persist to VMGS)
3200    pub no_persistent_secrets: bool,
3201    /// Hardware sealing policy for sealed secrets
3202    pub hardware_sealing_policy: PetriHardwareSealingPolicy,
3203    /// TPM reference implementation version
3204    pub version: PetriTpmVersion,
3205}
3206
3207impl Default for TpmConfig {
3208    fn default() -> Self {
3209        Self {
3210            no_persistent_secrets: true,
3211            hardware_sealing_policy: PetriHardwareSealingPolicy::Default,
3212            version: PetriTpmVersion::default(),
3213        }
3214    }
3215}
3216
3217/// TPM reference implementation version used by the test infrastructure.
3218#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
3219pub enum PetriTpmVersion {
3220    /// TPM reference implementation version 1.38
3221    V138,
3222    /// TPM reference implementation version 1.85
3223    #[default]
3224    V185,
3225}
3226
3227impl From<PetriTpmVersion> for tpm_resources::TpmVersion {
3228    fn from(version: PetriTpmVersion) -> Self {
3229        match version {
3230            PetriTpmVersion::V138 => tpm_resources::TpmVersion::V138,
3231            PetriTpmVersion::V185 => tpm_resources::TpmVersion::V185,
3232        }
3233    }
3234}
3235
3236impl From<PetriTpmVersion> for get_resources::ged::GedTpmVersion {
3237    fn from(version: PetriTpmVersion) -> Self {
3238        match version {
3239            PetriTpmVersion::V138 => get_resources::ged::GedTpmVersion::V138,
3240            PetriTpmVersion::V185 => get_resources::ged::GedTpmVersion::V185,
3241        }
3242    }
3243}
3244
3245/// Hardware sealing policy used by the test infrastructure.
3246///
3247/// Maps to Hyper-V `Set-GuestStateEncryptionPolicy` values and
3248/// underhill's `HardwareSealingPolicy`.
3249#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
3250pub enum PetriHardwareSealingPolicy {
3251    /// No explicit policy — the backend picks its default.
3252    #[default]
3253    Default,
3254    /// Derive the hardware sealing key from measurement hash.
3255    HashPolicy,
3256    /// Derive the hardware sealing key from signer information.
3257    SignerPolicy,
3258}
3259
3260/// Source for the IGVM firmware used to load OpenHCL.
3261#[derive(Debug, Clone)]
3262pub enum IgvmFirmwareSource {
3263    /// Load the IGVM from the specified file.
3264    File(ResolvedArtifact),
3265    /// Load the IGVM from VMGS, falling back to the in-box firmware.
3266    VmgsOrInBox,
3267}
3268
3269/// Firmware to load into the test VM.
3270// TODO: remove the guests from the firmware enum so that we don't pass them
3271// to the VMM backend after we have already used them generically.
3272#[derive(Debug, Clone)]
3273pub enum Firmware {
3274    /// Boot Linux directly, without any firmware.
3275    LinuxDirect {
3276        /// The kernel to boot.
3277        kernel: ResolvedArtifact,
3278        /// The initrd to use.
3279        initrd: ResolvedArtifact,
3280    },
3281    /// Boot an isolated Linux guest from an IGVM file with no paravisor.
3282    IgvmLinuxDirect {
3283        /// The IGVM file with the measured kernel, initrd, and bootshim.
3284        igvm_path: ResolvedArtifact,
3285        /// The isolation type of the guest.
3286        isolation: IsolationType,
3287    },
3288    /// Boot Linux directly, without any firmware, with OpenHCL in VTL2.
3289    OpenhclLinuxDirect {
3290        /// The source for the IGVM firmware.
3291        igvm_firmware: IgvmFirmwareSource,
3292        /// OpenHCL configuration
3293        openhcl_config: OpenHclConfig,
3294    },
3295    /// Boot a PCAT-based VM.
3296    Pcat {
3297        /// The guest OS the VM will boot into.
3298        guest: PcatGuest,
3299        /// The firmware to use.
3300        bios_firmware: ResolvedOptionalArtifact,
3301        /// The SVGA firmware to use.
3302        svga_firmware: ResolvedOptionalArtifact,
3303        /// IDE controllers and associated disks
3304        ide_controllers: [[Option<Drive>; 2]; 2],
3305    },
3306    /// Boot a PCAT-based VM with OpenHCL in VTL2.
3307    OpenhclPcat {
3308        /// The guest OS the VM will boot into.
3309        guest: PcatGuest,
3310        /// The source for the IGVM firmware.
3311        igvm_firmware: IgvmFirmwareSource,
3312        /// The firmware to use.
3313        bios_firmware: ResolvedOptionalArtifact,
3314        /// The SVGA firmware to use.
3315        svga_firmware: ResolvedOptionalArtifact,
3316        /// OpenHCL configuration
3317        openhcl_config: OpenHclConfig,
3318    },
3319    /// Boot a UEFI-based VM.
3320    Uefi {
3321        /// The guest OS the VM will boot into.
3322        guest: UefiGuest,
3323        /// The firmware to use.
3324        uefi_firmware: ResolvedArtifact,
3325        /// UEFI configuration
3326        uefi_config: UefiConfig,
3327    },
3328    /// Boot a UEFI-based VM with OpenHCL in VTL2.
3329    OpenhclUefi {
3330        /// The guest OS the VM will boot into.
3331        guest: UefiGuest,
3332        /// The isolation type of the VM.
3333        isolation: Option<IsolationType>,
3334        /// The source for the IGVM firmware.
3335        igvm_firmware: IgvmFirmwareSource,
3336        /// UEFI configuration
3337        uefi_config: UefiConfig,
3338        /// OpenHCL configuration
3339        openhcl_config: OpenHclConfig,
3340    },
3341}
3342
3343/// The boot device type.
3344#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3345pub enum BootDeviceType {
3346    /// Don't initialize a boot device.
3347    None,
3348    /// Boot from IDE.
3349    Ide,
3350    /// Boot from IDE via SCSI to VTL2.
3351    IdeViaScsi,
3352    /// Boot from IDE via NVME to VTL2.
3353    IdeViaNvme,
3354    /// Boot from SCSI.
3355    Scsi,
3356    /// Boot from SCSI via SCSI to VTL2.
3357    ScsiViaScsi,
3358    /// Boot from SCSI via NVME to VTL2.
3359    ScsiViaNvme,
3360    /// Boot from NVMe.
3361    Nvme,
3362    /// Boot from NVMe via SCSI to VTL2.
3363    NvmeViaScsi,
3364    /// Boot from NVMe via NVMe to VTL2.
3365    NvmeViaNvme,
3366    /// Boot from NVMe attached to a PCIe root port.
3367    PcieNvme,
3368    /// Boot from virtio-blk attached to a PCIe root port.
3369    PcieVirtioBlk,
3370}
3371
3372impl BootDeviceType {
3373    fn requires_vtl2(&self) -> bool {
3374        match self {
3375            BootDeviceType::None
3376            | BootDeviceType::Ide
3377            | BootDeviceType::Scsi
3378            | BootDeviceType::Nvme
3379            | BootDeviceType::PcieNvme
3380            | BootDeviceType::PcieVirtioBlk => false,
3381            BootDeviceType::IdeViaScsi
3382            | BootDeviceType::IdeViaNvme
3383            | BootDeviceType::ScsiViaScsi
3384            | BootDeviceType::ScsiViaNvme
3385            | BootDeviceType::NvmeViaScsi
3386            | BootDeviceType::NvmeViaNvme => true,
3387        }
3388    }
3389
3390    fn requires_vpci_boot(&self) -> bool {
3391        matches!(
3392            self,
3393            BootDeviceType::Nvme | BootDeviceType::NvmeViaScsi | BootDeviceType::NvmeViaNvme
3394        )
3395    }
3396
3397    fn requires_vmbus(&self) -> bool {
3398        match self {
3399            BootDeviceType::None
3400            | BootDeviceType::Ide
3401            | BootDeviceType::PcieNvme
3402            | BootDeviceType::PcieVirtioBlk => false,
3403            BootDeviceType::IdeViaScsi
3404            | BootDeviceType::IdeViaNvme
3405            | BootDeviceType::Scsi
3406            | BootDeviceType::ScsiViaScsi
3407            | BootDeviceType::ScsiViaNvme
3408            | BootDeviceType::Nvme
3409            | BootDeviceType::NvmeViaScsi
3410            | BootDeviceType::NvmeViaNvme => true,
3411        }
3412    }
3413}
3414
3415impl Firmware {
3416    /// Constructs a standard [`Firmware::LinuxDirect`] configuration, or a
3417    /// [`Firmware::IgvmLinuxDirect`] configuration if `isolation` is set.
3418    pub fn linux_direct(
3419        resolver: &ArtifactResolver<'_>,
3420        arch: MachineArch,
3421        isolation: Option<IsolationType>,
3422    ) -> Self {
3423        use petri_artifacts_vmm_test::artifacts::loadable::*;
3424        use petri_artifacts_vmm_test::artifacts::snp_igvm::SNP_LINUX_DIRECT_IGVM_X64;
3425        match (arch, isolation) {
3426            (MachineArch::X86_64, None) => Firmware::LinuxDirect {
3427                kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_X64).erase(),
3428                initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
3429            },
3430            (MachineArch::Aarch64, None) => Firmware::LinuxDirect {
3431                kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_AARCH64).erase(),
3432                initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_AARCH64).erase(),
3433            },
3434            (MachineArch::X86_64, Some(IsolationType::Snp)) => Firmware::IgvmLinuxDirect {
3435                igvm_path: resolver.require(SNP_LINUX_DIRECT_IGVM_X64).erase(),
3436                isolation: IsolationType::Snp,
3437            },
3438            (arch, Some(isolation)) => {
3439                panic!("Linux direct does not support {isolation:?} isolation on {arch:?}")
3440            }
3441        }
3442    }
3443
3444    /// Constructs a [`Firmware::LinuxDirect`] configuration that uses a
3445    /// compressed bzImage kernel instead of an uncompressed ELF.
3446    ///
3447    /// This is x86_64-only, as bzImage is an x86-specific format.
3448    pub fn linux_direct_bzimage(resolver: &ArtifactResolver<'_>) -> Self {
3449        use petri_artifacts_vmm_test::artifacts::loadable::*;
3450        Firmware::LinuxDirect {
3451            kernel: resolver.require(LINUX_DIRECT_TEST_BZIMAGE_X64).erase(),
3452            initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
3453        }
3454    }
3455
3456    /// Constructs a standard [`Firmware::OpenhclLinuxDirect`] configuration.
3457    pub fn openhcl_linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
3458        use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
3459        match arch {
3460            MachineArch::X86_64 => Firmware::OpenhclLinuxDirect {
3461                igvm_firmware: IgvmFirmwareSource::File(
3462                    resolver.require(LATEST_LINUX_DIRECT_TEST_X64).erase(),
3463                ),
3464                openhcl_config: Default::default(),
3465            },
3466            MachineArch::Aarch64 => todo!("Linux direct not yet supported on aarch64"),
3467        }
3468    }
3469
3470    /// Constructs a standard [`Firmware::Pcat`] configuration.
3471    pub fn pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
3472        use petri_artifacts_vmm_test::artifacts::loadable::*;
3473        Firmware::Pcat {
3474            guest,
3475            bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
3476            svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
3477            ide_controllers: [[None, None], [None, None]],
3478        }
3479    }
3480
3481    /// Constructs a standard [`Firmware::OpenhclPcat`] configuration.
3482    pub fn openhcl_pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
3483        use petri_artifacts_vmm_test::artifacts::loadable::*;
3484        use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
3485        Firmware::OpenhclPcat {
3486            guest,
3487            igvm_firmware: IgvmFirmwareSource::File(resolver.require(LATEST_STANDARD_X64).erase()),
3488            bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
3489            svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
3490            openhcl_config: OpenHclConfig {
3491                // VMBUS redirect is necessary for IDE to be provided by VTL2
3492                vmbus_redirect: true,
3493                ..Default::default()
3494            },
3495        }
3496    }
3497
3498    /// Constructs a standard [`Firmware::Uefi`] configuration.
3499    pub fn uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch, guest: UefiGuest) -> Self {
3500        use petri_artifacts_vmm_test::artifacts::loadable::*;
3501        let uefi_firmware = match arch {
3502            MachineArch::X86_64 => resolver.require(UEFI_FIRMWARE_X64).erase(),
3503            MachineArch::Aarch64 => resolver.require(UEFI_FIRMWARE_AARCH64).erase(),
3504        };
3505        Firmware::Uefi {
3506            guest,
3507            uefi_firmware,
3508            uefi_config: Default::default(),
3509        }
3510    }
3511
3512    /// Constructs a standard [`Firmware::OpenhclUefi`] configuration.
3513    pub fn openhcl_uefi(
3514        resolver: &ArtifactResolver<'_>,
3515        arch: MachineArch,
3516        guest: UefiGuest,
3517        isolation: Option<IsolationType>,
3518    ) -> Self {
3519        use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
3520        let igvm_firmware = IgvmFirmwareSource::File(match arch {
3521            MachineArch::X86_64 if isolation.is_some() => resolver.require(LATEST_CVM_X64).erase(),
3522            MachineArch::X86_64 => resolver.require(LATEST_STANDARD_X64).erase(),
3523            MachineArch::Aarch64 => resolver.require(LATEST_STANDARD_AARCH64).erase(),
3524        });
3525        Firmware::OpenhclUefi {
3526            guest,
3527            isolation,
3528            igvm_firmware,
3529            uefi_config: Default::default(),
3530            openhcl_config: Default::default(),
3531        }
3532    }
3533
3534    fn is_openhcl(&self) -> bool {
3535        match self {
3536            Firmware::OpenhclLinuxDirect { .. }
3537            | Firmware::OpenhclUefi { .. }
3538            | Firmware::OpenhclPcat { .. } => true,
3539            Firmware::LinuxDirect { .. }
3540            | Firmware::IgvmLinuxDirect { .. }
3541            | Firmware::Pcat { .. }
3542            | Firmware::Uefi { .. } => false,
3543        }
3544    }
3545
3546    fn isolation(&self) -> Option<IsolationType> {
3547        match self {
3548            Firmware::IgvmLinuxDirect { isolation, .. } => Some(*isolation),
3549            Firmware::OpenhclUefi { isolation, .. } => *isolation,
3550            Firmware::LinuxDirect { .. }
3551            | Firmware::Pcat { .. }
3552            | Firmware::Uefi { .. }
3553            | Firmware::OpenhclLinuxDirect { .. }
3554            | Firmware::OpenhclPcat { .. } => None,
3555        }
3556    }
3557
3558    /// Isolated guest with no paravisor
3559    fn is_isolated_without_paravisor(&self) -> bool {
3560        self.isolation().is_some() && !self.is_openhcl()
3561    }
3562
3563    fn is_linux_direct(&self) -> bool {
3564        match self {
3565            Firmware::LinuxDirect { .. }
3566            | Firmware::IgvmLinuxDirect { .. }
3567            | Firmware::OpenhclLinuxDirect { .. } => true,
3568            Firmware::Pcat { .. }
3569            | Firmware::Uefi { .. }
3570            | Firmware::OpenhclUefi { .. }
3571            | Firmware::OpenhclPcat { .. } => false,
3572        }
3573    }
3574
3575    /// Get the initrd path for Linux direct boot firmware.
3576    pub fn linux_direct_initrd(&self) -> Option<&Path> {
3577        match self {
3578            Firmware::LinuxDirect { initrd, .. } => Some(initrd.get()),
3579            _ => None,
3580        }
3581    }
3582
3583    fn is_pcat(&self) -> bool {
3584        match self {
3585            Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => true,
3586            Firmware::Uefi { .. }
3587            | Firmware::OpenhclUefi { .. }
3588            | Firmware::LinuxDirect { .. }
3589            | Firmware::IgvmLinuxDirect { .. }
3590            | Firmware::OpenhclLinuxDirect { .. } => false,
3591        }
3592    }
3593
3594    fn os_flavor(&self) -> OsFlavor {
3595        match self {
3596            Firmware::LinuxDirect { .. }
3597            | Firmware::OpenhclLinuxDirect { .. }
3598            | Firmware::Uefi {
3599                guest: UefiGuest::Mshv(_),
3600                ..
3601            }
3602            | Firmware::OpenhclUefi {
3603                guest: UefiGuest::Mshv(_),
3604                ..
3605            }
3606            | Firmware::IgvmLinuxDirect { .. } => OsFlavor::Linux,
3607            Firmware::Uefi {
3608                guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
3609                ..
3610            }
3611            | Firmware::OpenhclUefi {
3612                guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
3613                ..
3614            } => OsFlavor::Uefi,
3615            Firmware::Pcat {
3616                guest: PcatGuest::Vhd(cfg),
3617                ..
3618            }
3619            | Firmware::OpenhclPcat {
3620                guest: PcatGuest::Vhd(cfg),
3621                ..
3622            }
3623            | Firmware::Uefi {
3624                guest: UefiGuest::Vhd(cfg),
3625                ..
3626            }
3627            | Firmware::OpenhclUefi {
3628                guest: UefiGuest::Vhd(cfg),
3629                ..
3630            } => cfg.os_flavor,
3631            Firmware::Pcat {
3632                guest: PcatGuest::Iso(cfg),
3633                ..
3634            }
3635            | Firmware::OpenhclPcat {
3636                guest: PcatGuest::Iso(cfg),
3637                ..
3638            } => cfg.os_flavor,
3639        }
3640    }
3641
3642    fn quirks(&self) -> GuestQuirks {
3643        match self {
3644            Firmware::Pcat {
3645                guest: PcatGuest::Vhd(cfg),
3646                ..
3647            }
3648            | Firmware::Uefi {
3649                guest: UefiGuest::Vhd(cfg),
3650                ..
3651            }
3652            | Firmware::OpenhclUefi {
3653                guest: UefiGuest::Vhd(cfg),
3654                ..
3655            } => cfg.quirks.clone(),
3656            Firmware::Pcat {
3657                guest: PcatGuest::Iso(cfg),
3658                ..
3659            } => cfg.quirks.clone(),
3660            _ => Default::default(),
3661        }
3662    }
3663
3664    fn expected_boot_event(&self) -> Option<FirmwareEvent> {
3665        match self {
3666            Firmware::LinuxDirect { .. }
3667            | Firmware::IgvmLinuxDirect { .. }
3668            | Firmware::OpenhclLinuxDirect { .. }
3669            | Firmware::Uefi {
3670                guest: UefiGuest::GuestTestUefi(_),
3671                ..
3672            }
3673            | Firmware::OpenhclUefi {
3674                guest: UefiGuest::GuestTestUefi(_),
3675                ..
3676            } => None,
3677            Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => {
3678                // TODO: Handle older PCAT versions that don't fire the event
3679                Some(FirmwareEvent::BootAttempt)
3680            }
3681            Firmware::Uefi {
3682                guest: UefiGuest::None,
3683                ..
3684            }
3685            | Firmware::OpenhclUefi {
3686                guest: UefiGuest::None,
3687                ..
3688            } => Some(FirmwareEvent::NoBootDevice),
3689            Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => {
3690                Some(FirmwareEvent::BootSuccess)
3691            }
3692        }
3693    }
3694
3695    fn openhcl_config(&self) -> Option<&OpenHclConfig> {
3696        match self {
3697            Firmware::OpenhclLinuxDirect { openhcl_config, .. }
3698            | Firmware::OpenhclUefi { openhcl_config, .. }
3699            | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
3700            Firmware::LinuxDirect { .. }
3701            | Firmware::IgvmLinuxDirect { .. }
3702            | Firmware::Pcat { .. }
3703            | Firmware::Uefi { .. } => None,
3704        }
3705    }
3706
3707    fn openhcl_config_mut(&mut self) -> Option<&mut OpenHclConfig> {
3708        match self {
3709            Firmware::OpenhclLinuxDirect { openhcl_config, .. }
3710            | Firmware::OpenhclUefi { openhcl_config, .. }
3711            | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
3712            Firmware::LinuxDirect { .. }
3713            | Firmware::IgvmLinuxDirect { .. }
3714            | Firmware::Pcat { .. }
3715            | Firmware::Uefi { .. } => None,
3716        }
3717    }
3718
3719    fn openhcl_firmware(&self) -> Option<&IgvmFirmwareSource> {
3720        match self {
3721            Firmware::OpenhclLinuxDirect { igvm_firmware, .. }
3722            | Firmware::OpenhclUefi { igvm_firmware, .. }
3723            | Firmware::OpenhclPcat { igvm_firmware, .. } => Some(igvm_firmware),
3724            Firmware::LinuxDirect { .. }
3725            | Firmware::IgvmLinuxDirect { .. }
3726            | Firmware::Pcat { .. }
3727            | Firmware::Uefi { .. } => None,
3728        }
3729    }
3730
3731    fn into_runtime_config(
3732        self,
3733        vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
3734    ) -> PetriVmRuntimeConfig {
3735        match self {
3736            Firmware::OpenhclLinuxDirect { openhcl_config, .. }
3737            | Firmware::OpenhclUefi { openhcl_config, .. }
3738            | Firmware::OpenhclPcat { openhcl_config, .. } => PetriVmRuntimeConfig {
3739                vtl2_settings: Some(
3740                    openhcl_config
3741                        .vtl2_settings
3742                        .unwrap_or_else(default_vtl2_settings),
3743                ),
3744                ide_controllers: None,
3745                vmbus_storage_controllers,
3746            },
3747            Firmware::Pcat {
3748                ide_controllers, ..
3749            } => PetriVmRuntimeConfig {
3750                vtl2_settings: None,
3751                ide_controllers: Some(ide_controllers),
3752                vmbus_storage_controllers,
3753            },
3754            Firmware::LinuxDirect { .. }
3755            | Firmware::IgvmLinuxDirect { .. }
3756            | Firmware::Uefi { .. } => PetriVmRuntimeConfig {
3757                vtl2_settings: None,
3758                ide_controllers: None,
3759                vmbus_storage_controllers,
3760            },
3761        }
3762    }
3763
3764    fn uefi_config(&self) -> Option<&UefiConfig> {
3765        match self {
3766            Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
3767                Some(uefi_config)
3768            }
3769            Firmware::LinuxDirect { .. }
3770            | Firmware::IgvmLinuxDirect { .. }
3771            | Firmware::OpenhclLinuxDirect { .. }
3772            | Firmware::Pcat { .. }
3773            | Firmware::OpenhclPcat { .. } => None,
3774        }
3775    }
3776
3777    fn uefi_config_mut(&mut self) -> Option<&mut UefiConfig> {
3778        match self {
3779            Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
3780                Some(uefi_config)
3781            }
3782            Firmware::LinuxDirect { .. }
3783            | Firmware::IgvmLinuxDirect { .. }
3784            | Firmware::OpenhclLinuxDirect { .. }
3785            | Firmware::Pcat { .. }
3786            | Firmware::OpenhclPcat { .. } => None,
3787        }
3788    }
3789
3790    fn boot_drive(&self, use_virtio_vsock: bool) -> anyhow::Result<Option<Drive>> {
3791        if let Firmware::Uefi {
3792            guest: UefiGuest::Mshv(boot),
3793            ..
3794        }
3795        | Firmware::OpenhclUefi {
3796            guest: UefiGuest::Mshv(boot),
3797            ..
3798        } = self
3799        {
3800            return Ok(Some(Drive::new(
3801                Some(
3802                    boot.build(use_virtio_vsock)
3803                        .context("composing mshv boot disk")?,
3804                ),
3805                false,
3806            )));
3807        }
3808        Ok(match self {
3809            Firmware::LinuxDirect { .. }
3810            | Firmware::IgvmLinuxDirect { .. }
3811            | Firmware::OpenhclLinuxDirect { .. } => None,
3812            Firmware::Pcat { guest, .. } | Firmware::OpenhclPcat { guest, .. } => {
3813                Some((guest.disk_path(), guest.is_dvd()))
3814            }
3815            Firmware::Uefi { guest, .. } | Firmware::OpenhclUefi { guest, .. } => {
3816                guest.disk_path().map(|dp| (dp, false))
3817            }
3818        }
3819        .map(|(disk_path, is_dvd)| Drive::new(Some(Disk::Differencing(disk_path)), is_dvd)))
3820    }
3821
3822    fn vtl2_settings(&mut self) -> Option<&mut Vtl2Settings> {
3823        self.openhcl_config_mut()
3824            .map(|c| c.vtl2_settings.get_or_insert_with(default_vtl2_settings))
3825    }
3826
3827    fn ide_controllers(&self) -> Option<&[[Option<Drive>; 2]; 2]> {
3828        match self {
3829            Firmware::Pcat {
3830                ide_controllers, ..
3831            } => Some(ide_controllers),
3832            _ => None,
3833        }
3834    }
3835
3836    fn ide_controllers_mut(&mut self) -> Option<&mut [[Option<Drive>; 2]; 2]> {
3837        match self {
3838            Firmware::Pcat {
3839                ide_controllers, ..
3840            } => Some(ide_controllers),
3841            _ => None,
3842        }
3843    }
3844}
3845
3846/// The guest the VM will boot into. A boot drive with the chosen setup
3847/// will be automatically configured.
3848#[derive(Debug, Clone)]
3849pub enum PcatGuest {
3850    /// Mount a VHD as the boot drive.
3851    Vhd(BootImageConfig<boot_image_type::Vhd>),
3852    /// Mount an ISO as the CD/DVD drive.
3853    Iso(BootImageConfig<boot_image_type::Iso>),
3854}
3855
3856impl PcatGuest {
3857    fn disk_path(&self) -> DiskPath {
3858        match self {
3859            PcatGuest::Vhd(disk) => disk.disk_path(),
3860            PcatGuest::Iso(disk) => disk.disk_path(),
3861        }
3862    }
3863
3864    fn is_dvd(&self) -> bool {
3865        matches!(self, Self::Iso(_))
3866    }
3867}
3868
3869/// The guest the VM will boot into. A boot drive with the chosen setup
3870/// will be automatically configured.
3871#[derive(Debug, Clone)]
3872pub enum UefiGuest {
3873    /// Compose an mshv root-partition boot disk from resolved artifacts and a
3874    /// prepared initrd. Requires x86-64 UEFI without OpenHCL.
3875    Mshv(MshvBoot),
3876    /// Mount a VHD as the boot drive.
3877    Vhd(BootImageConfig<boot_image_type::Vhd>),
3878    /// The UEFI test image produced by our guest-test infrastructure.
3879    GuestTestUefi(ResolvedArtifact),
3880    /// No guest, just the firmware.
3881    None,
3882}
3883
3884impl UefiGuest {
3885    /// Construct a standard [`UefiGuest::GuestTestUefi`] configuration.
3886    pub fn guest_test_uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
3887        use petri_artifacts_vmm_test::artifacts::test_vhd::*;
3888        let artifact = match arch {
3889            MachineArch::X86_64 => resolver.require(GUEST_TEST_UEFI_X64).erase(),
3890            MachineArch::Aarch64 => resolver.require(GUEST_TEST_UEFI_AARCH64).erase(),
3891        };
3892        UefiGuest::GuestTestUefi(artifact)
3893    }
3894
3895    fn disk_path(&self) -> Option<DiskPath> {
3896        match self {
3897            UefiGuest::Vhd(vhd) => Some(vhd.disk_path()),
3898            UefiGuest::GuestTestUefi(p) => Some(DiskPath::Local(p.get().to_path_buf())),
3899            UefiGuest::None => None,
3900            UefiGuest::Mshv(_) => None,
3901        }
3902    }
3903}
3904
3905/// Type-tags for [`BootImageConfig`](super::BootImageConfig)
3906pub mod boot_image_type {
3907    mod private {
3908        pub trait Sealed {}
3909        impl Sealed for super::Vhd {}
3910        impl Sealed for super::Iso {}
3911    }
3912
3913    /// Private trait use to seal the set of artifact types BootImageType
3914    /// supports.
3915    pub trait BootImageType: private::Sealed {}
3916
3917    /// BootImageConfig for a VHD file
3918    #[derive(Debug, Clone)]
3919    pub enum Vhd {}
3920
3921    /// BootImageConfig for an ISO file
3922    #[derive(Debug, Clone)]
3923    pub enum Iso {}
3924
3925    impl BootImageType for Vhd {}
3926    impl BootImageType for Iso {}
3927}
3928
3929/// Configuration information for the boot drive of the VM.
3930#[derive(Debug, Clone)]
3931pub struct BootImageConfig<T: boot_image_type::BootImageType> {
3932    /// Artifact source corresponding to the boot media (local or remote).
3933    artifact: ResolvedArtifactSource,
3934    /// The OS flavor.
3935    os_flavor: OsFlavor,
3936    /// Any quirks needed to boot the guest.
3937    ///
3938    /// Most guests should not need any quirks, and can use `Default`.
3939    quirks: GuestQuirks,
3940    /// Marker denoting what type of media `artifact` corresponds to
3941    _type: core::marker::PhantomData<T>,
3942}
3943
3944impl<T: boot_image_type::BootImageType> BootImageConfig<T> {
3945    /// Get a [`DiskPath`] from the artifact source.
3946    fn disk_path(&self) -> DiskPath {
3947        match self.artifact.get() {
3948            ArtifactSource::Local(p) => DiskPath::Local(p.clone()),
3949            ArtifactSource::Remote { url } => DiskPath::Remote { url: url.clone() },
3950        }
3951    }
3952}
3953
3954impl BootImageConfig<boot_image_type::Vhd> {
3955    /// Create a new BootImageConfig from a VHD artifact source
3956    pub fn from_vhd<A>(artifact: ResolvedArtifactSource<A>) -> Self
3957    where
3958        A: petri_artifacts_common::tags::IsTestVhd,
3959    {
3960        BootImageConfig {
3961            artifact: artifact.erase(),
3962            os_flavor: A::OS_FLAVOR,
3963            quirks: A::quirks(),
3964            _type: std::marker::PhantomData,
3965        }
3966    }
3967}
3968
3969impl BootImageConfig<boot_image_type::Iso> {
3970    /// Create a new BootImageConfig from an ISO artifact source
3971    pub fn from_iso<A>(artifact: ResolvedArtifactSource<A>) -> Self
3972    where
3973        A: petri_artifacts_common::tags::IsTestIso,
3974    {
3975        BootImageConfig {
3976            artifact: artifact.erase(),
3977            os_flavor: A::OS_FLAVOR,
3978            quirks: A::quirks(),
3979            _type: std::marker::PhantomData,
3980        }
3981    }
3982}
3983
3984/// Isolation type
3985#[derive(Debug, Clone, Copy)]
3986pub enum IsolationType {
3987    /// VBS
3988    Vbs,
3989    /// SNP
3990    Snp,
3991    /// TDX
3992    Tdx,
3993}
3994
3995/// Flags controlling servicing behavior.
3996#[derive(Debug, Clone, Copy)]
3997pub struct OpenHclServicingFlags {
3998    /// Preserve DMA memory for NVMe devices if supported.
3999    /// Defaults to `true`.
4000    pub enable_nvme_keepalive: bool,
4001    /// Preserve DMA memory for MANA devices if supported.
4002    pub enable_mana_keepalive: bool,
4003    /// 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.
4004    pub override_version_checks: bool,
4005    /// Hint to the OpenHCL runtime how much time to wait when stopping / saving the OpenHCL.
4006    pub stop_timeout_hint_secs: Option<u16>,
4007}
4008
4009/// Where a disk image is located.
4010#[derive(Debug, Clone)]
4011pub enum DiskPath {
4012    /// A local file path.
4013    Local(PathBuf),
4014    /// A remote URL (fetched on demand via HTTP Range requests).
4015    Remote {
4016        /// The URL where the disk can be fetched.
4017        url: String,
4018    },
4019}
4020
4021impl From<PathBuf> for DiskPath {
4022    fn from(path: PathBuf) -> Self {
4023        DiskPath::Local(path)
4024    }
4025}
4026
4027/// Petri disk
4028#[derive(Debug, Clone)]
4029pub enum Disk {
4030    /// Memory backed with specified size
4031    Memory(u64),
4032    /// Memory differencing disk backed by a VHD (local or remote)
4033    Differencing(DiskPath),
4034    /// Differencing disk backed by a VHD (local or remote) that stores writes
4035    /// in a SQLite file, so they persist across VM worker processes. The file
4036    /// is created on first use.
4037    SqliteDifferencing {
4038        /// The base disk.
4039        base: DiskPath,
4040        /// The SQLite file that stores the writes.
4041        dbhd_path: PathBuf,
4042    },
4043    /// Persistent VHD
4044    Persistent(PathBuf),
4045    /// Disk backed by a temporary VHD
4046    Temporary(Arc<TempPath>),
4047}
4048
4049/// A path that can be either temporary or persistent
4050#[derive(Debug, Clone)]
4051pub enum TempOrPersistentPath {
4052    /// Temporary path
4053    Temp(Arc<TempPath>),
4054    /// Persistent path
4055    Persistent(PathBuf),
4056}
4057
4058impl AsRef<Path> for TempOrPersistentPath {
4059    fn as_ref(&self) -> &Path {
4060        match self {
4061            TempOrPersistentPath::Temp(temp_path) => temp_path.as_ref(),
4062            TempOrPersistentPath::Persistent(path_buf) => path_buf.as_ref(),
4063        }
4064    }
4065}
4066
4067/// Petri initrd
4068#[derive(Debug, Clone)]
4069pub struct PetriInitrd {
4070    /// Where the initrd is located on the host system.
4071    pub path: TempOrPersistentPath,
4072    /// Path to use with `rdinit=` kernel command line parameter.
4073    pub rdinit_param: String,
4074}
4075
4076/// Petri VMGS disk
4077#[derive(Debug, Clone)]
4078pub struct PetriVmgsDisk {
4079    /// Backing disk
4080    pub disk: Disk,
4081    /// Guest state encryption policy
4082    pub encryption_policy: GuestStateEncryptionPolicy,
4083}
4084
4085impl Default for PetriVmgsDisk {
4086    fn default() -> Self {
4087        PetriVmgsDisk {
4088            disk: Disk::Memory(vmgs_format::VMGS_DEFAULT_CAPACITY),
4089            // TODO: make this strict once we can set it in OpenHCL on Hyper-V
4090            encryption_policy: GuestStateEncryptionPolicy::None(false),
4091        }
4092    }
4093}
4094
4095/// Petri VM guest state resource
4096#[derive(Debug, Clone)]
4097pub enum PetriVmgsResource {
4098    /// Use disk to store guest state
4099    Disk(PetriVmgsDisk),
4100    /// Use disk to store guest state, reformatting if corrupted.
4101    ReprovisionOnFailure(PetriVmgsDisk),
4102    /// Format and use disk to store guest state
4103    ReprovisionSingle(PetriVmgsDisk),
4104    /// Store guest state in memory
4105    Ephemeral,
4106}
4107
4108impl PetriVmgsResource {
4109    /// get the inner vmgs disk if one exists
4110    pub fn vmgs(&self) -> Option<&PetriVmgsDisk> {
4111        match self {
4112            PetriVmgsResource::Disk(vmgs)
4113            | PetriVmgsResource::ReprovisionOnFailure(vmgs)
4114            | PetriVmgsResource::ReprovisionSingle(vmgs) => Some(vmgs),
4115            PetriVmgsResource::Ephemeral => None,
4116        }
4117    }
4118
4119    /// get the inner disk if one exists
4120    pub fn disk(&self) -> Option<&Disk> {
4121        self.vmgs().map(|vmgs| &vmgs.disk)
4122    }
4123
4124    /// get the encryption policy of the vmgs
4125    pub fn encryption_policy(&self) -> Option<GuestStateEncryptionPolicy> {
4126        self.vmgs().map(|vmgs| vmgs.encryption_policy)
4127    }
4128}
4129
4130/// Petri VM guest state lifetime
4131#[derive(Debug, Clone, Copy)]
4132pub enum PetriGuestStateLifetime {
4133    /// Use a differencing disk backed by a blank, tempory VMGS file
4134    /// or other artifact if one is provided
4135    Disk,
4136    /// Same as default, except reformat the backing disk if corrupted
4137    ReprovisionOnFailure,
4138    /// Same as default, except reformat the backing disk
4139    ReprovisionSingle,
4140    /// Store guest state in memory (no backing disk)
4141    Ephemeral,
4142}
4143
4144/// UEFI secure boot template
4145#[derive(Debug, Clone, Copy)]
4146pub enum SecureBootTemplate {
4147    /// The Microsoft Windows template.
4148    MicrosoftWindows,
4149    /// The Microsoft UEFI certificate authority template.
4150    MicrosoftUefiCertificateAuthority,
4151}
4152
4153/// Quirks to workaround certain bugs that only manifest when using a
4154/// particular VMM, and do not depend on which guest is running.
4155#[derive(Default, Debug, Clone)]
4156pub struct VmmQuirks {
4157    /// Automatically reset the VM if we did not recieve a boot event in the
4158    /// specified amount of time.
4159    pub flaky_boot: Option<Duration>,
4160}
4161
4162/// The reason that the VM halted
4163#[derive(Debug, Clone, Copy, Eq, PartialEq)]
4164pub enum PetriHaltReason {
4165    /// The vm powered off
4166    PowerOff,
4167    /// The vm reset
4168    Reset,
4169    /// The vm hibernated
4170    Hibernate,
4171    /// The vm triple faulted
4172    TripleFault,
4173    /// The vm halted for some other reason
4174    Other,
4175}
4176
4177impl PetriHaltReason {
4178    /// Construct a halt reason with detailed debug info
4179    pub fn with_detail(self, detail: String) -> PetriHaltReasonDetail {
4180        PetriHaltReasonDetail {
4181            reason: self,
4182            detail,
4183        }
4184    }
4185}
4186
4187/// The reason that the VM halted, with optional addition debug details
4188#[derive(Debug, Clone)]
4189pub struct PetriHaltReasonDetail {
4190    /// The reason for the halt
4191    pub reason: PetriHaltReason,
4192    /// More details about the halt
4193    pub detail: String,
4194}
4195
4196pub(crate) fn linux_vsock_blacklist(use_virtio_vsock: bool) -> &'static str {
4197    if use_virtio_vsock {
4198        "initcall_blacklist=hv_sock_init"
4199    } else {
4200        "initcall_blacklist=virtio_vsock_init"
4201    }
4202}
4203
4204fn append_cmdline(cmd: &mut Option<String>, add_cmd: impl AsRef<str>) {
4205    if let Some(cmd) = cmd.as_mut() {
4206        cmd.push(' ');
4207        cmd.push_str(add_cmd.as_ref());
4208    } else {
4209        *cmd = Some(add_cmd.as_ref().to_string());
4210    }
4211}
4212
4213async fn collect_inspect(
4214    name: &'static str,
4215    inspect: impl Future<Output = anyhow::Result<inspect::Node>>,
4216    log_source: &PetriLogSource,
4217    prefix: &str,
4218) {
4219    let node = match CancelContext::new()
4220        .with_timeout(INSPECT_TIMEOUT)
4221        .until_cancelled(inspect)
4222        .await
4223    {
4224        Ok(Ok(node)) => node,
4225        Ok(Err(e)) => {
4226            tracing::warn!(name, ?e, "failed to collect inspect state");
4227            return;
4228        }
4229        Err(_) => {
4230            tracing::warn!(name, "timed out collecting inspect state");
4231            return;
4232        }
4233    };
4234
4235    if let Err(e) = log_source.write_attachment(
4236        &format!("{prefix}_inspect_{name}.log"),
4237        format!("{node:#}").as_bytes(),
4238    ) {
4239        tracing::error!(name, ?e, "failed to save inspect log");
4240    }
4241}
4242
4243/// Wrapper for modification functions with stubbed out debug impl
4244pub struct ModifyFn<T>(pub Box<dyn FnOnce(T) -> T + Send>);
4245
4246impl<T> Debug for ModifyFn<T> {
4247    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4248        write!(f, "_")
4249    }
4250}
4251
4252/// Default VTL 2 settings used by petri
4253fn default_vtl2_settings() -> Vtl2Settings {
4254    Vtl2Settings {
4255        version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
4256        fixed: None,
4257        dynamic: Some(Default::default()),
4258        namespace_settings: Default::default(),
4259    }
4260}
4261
4262/// Virtual trust level
4263#[derive(Debug, Copy, Clone, PartialEq, Eq)]
4264pub enum Vtl {
4265    /// VTL 0
4266    Vtl0 = 0,
4267    /// VTL 1
4268    Vtl1 = 1,
4269    /// VTL 2
4270    Vtl2 = 2,
4271}
4272
4273/// The VMBus storage device type.
4274#[derive(Debug, Copy, Clone, PartialEq, Eq)]
4275pub enum VmbusStorageType {
4276    /// SCSI
4277    Scsi,
4278    /// NVMe
4279    Nvme,
4280    /// Virtio block device
4281    VirtioBlk,
4282}
4283
4284/// VM disk drive
4285#[derive(Debug, Clone)]
4286pub struct Drive {
4287    /// Backing disk
4288    pub disk: Option<Disk>,
4289    /// Whether this is a DVD
4290    pub is_dvd: bool,
4291}
4292
4293impl Drive {
4294    /// Create a new disk
4295    pub fn new(disk: Option<Disk>, is_dvd: bool) -> Self {
4296        Self { disk, is_dvd }
4297    }
4298}
4299
4300/// VMBus storage controller
4301#[derive(Debug, Clone)]
4302pub struct VmbusStorageController {
4303    /// The VTL to assign the storage controller to
4304    pub target_vtl: Vtl,
4305    /// The storage device type
4306    pub controller_type: VmbusStorageType,
4307    /// Drives (with any inserted disks) attached to this storage controller
4308    pub drives: HashMap<u32, Drive>,
4309}
4310
4311impl VmbusStorageController {
4312    /// Create a new storage controller
4313    pub fn new(target_vtl: Vtl, controller_type: VmbusStorageType) -> Self {
4314        Self {
4315            target_vtl,
4316            controller_type,
4317            drives: HashMap::new(),
4318        }
4319    }
4320
4321    /// Add a disk to the storage controller
4322    pub fn set_drive(
4323        &mut self,
4324        lun: Option<u32>,
4325        drive: Drive,
4326        allow_modify_existing: bool,
4327    ) -> u32 {
4328        let lun = lun.unwrap_or_else(|| {
4329            // find the first available lun
4330            let mut lun = None;
4331            for x in 0..u8::MAX as u32 {
4332                if !self.drives.contains_key(&x) {
4333                    lun = Some(x);
4334                    break;
4335                }
4336            }
4337            lun.expect("all locations on this controller are in use")
4338        });
4339
4340        if self.drives.insert(lun, drive).is_some() && !allow_modify_existing {
4341            panic!("a disk with lun {lun} already existed on this controller");
4342        }
4343
4344        lun
4345    }
4346}
4347
4348/// Returns the cache directory for lazy-fetched disk artifacts.
4349pub(crate) fn petri_disk_cache_dir() -> String {
4350    if let Ok(dir) = std::env::var("PETRI_CACHE_DIR") {
4351        return dir;
4352    }
4353
4354    #[cfg(target_os = "macos")]
4355    {
4356        if let Ok(home) = std::env::var("HOME") {
4357            return format!("{home}/Library/Caches/petri");
4358        }
4359    }
4360
4361    #[cfg(windows)]
4362    {
4363        if let Ok(local) = std::env::var("LOCALAPPDATA") {
4364            return format!("{local}\\petri\\cache");
4365        }
4366    }
4367
4368    // Linux / fallback: XDG
4369    if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
4370        return format!("{xdg}/petri");
4371    }
4372    if let Ok(home) = std::env::var("HOME") {
4373        return format!("{home}/.cache/petri");
4374    }
4375
4376    ".cache/petri".to_string()
4377}
4378
4379/// All the information needed to locate and run a particular Nextest test
4380pub struct NestedTestDeps {
4381    /// Nextest archive artifact
4382    pub archive: ErasedArtifactHandle,
4383    /// Binary name within the nextest artifact
4384    pub binary: String,
4385    /// Test name within the binary
4386    pub test_name: String,
4387}
4388
4389impl NestedTestDeps {
4390    /// Create a new set of nested test dep arguments
4391    pub fn new<A: IsNextestArchive>(
4392        archive: ArtifactHandle<A>,
4393        binary: &str,
4394        test_name: &str,
4395    ) -> Self {
4396        Self {
4397            archive: archive.erase(),
4398            binary: binary.into(),
4399            test_name: test_name.into(),
4400        }
4401    }
4402}
4403
4404/// All the information needed to locate and run a particular Nextest test
4405pub struct ResolvedNestedTestDeps {
4406    /// Extracted test binary
4407    pub test_binary: TempPath,
4408    /// Test name within the binary
4409    pub test_name: String,
4410    /// Artifacts required by the test
4411    pub artifacts: Vec<(ErasedArtifactHandle, PathBuf)>,
4412}
4413
4414mod nextest_binaries_metadata_schema {
4415    use serde::Deserialize;
4416    use serde::Serialize;
4417    use std::collections::HashMap;
4418    use std::path::PathBuf;
4419
4420    #[derive(Serialize, Deserialize)]
4421    #[serde(rename_all = "kebab-case")]
4422    pub struct RustBuildMeta {
4423        pub target_directory: PathBuf,
4424    }
4425
4426    #[derive(Serialize, Deserialize)]
4427    #[serde(rename_all = "kebab-case")]
4428    pub struct Binary {
4429        pub binary_name: String,
4430        pub binary_path: PathBuf,
4431    }
4432
4433    #[derive(Serialize, Deserialize)]
4434    #[serde(rename_all = "kebab-case")]
4435    pub struct BinariesMetadata {
4436        pub rust_build_meta: RustBuildMeta,
4437        pub rust_binaries: HashMap<String, Binary>,
4438    }
4439}
4440
4441impl NestedTestDeps {
4442    /// Extract the binary from the archive by parsing the json metadata
4443    fn extract_binary(&self) -> anyhow::Result<TempPath> {
4444        let temp_dir = tempfile::tempdir()?;
4445
4446        // We need to resolve this manually since we need to get the nested test
4447        // artifacts and to do that we need the archive first.
4448        let archive = resolve_artifact(self.archive)?;
4449
4450        let mut cmd = std::process::Command::new("tar");
4451        cmd.arg("--zstd")
4452            .arg("-xvf")
4453            .arg(archive)
4454            .current_dir(&temp_dir);
4455        futures::executor::block_on(crate::run_host_cmd(cmd))?;
4456
4457        let meta_file = fs_err::File::open(
4458            temp_dir
4459                .path()
4460                .join("target")
4461                .join("nextest")
4462                .join("binaries-metadata.json"),
4463        )?;
4464        let meta: nextest_binaries_metadata_schema::BinariesMetadata =
4465            serde_json::from_reader(meta_file)?;
4466
4467        let build_binary_path = meta
4468            .rust_binaries
4469            .iter()
4470            .find(|(_, b)| b.binary_name == self.binary)
4471            .context("binary missing")?
4472            .1
4473            .binary_path
4474            .clone();
4475
4476        let local_binary_path = temp_dir.path().join(
4477            build_binary_path
4478                .strip_prefix(meta.rust_build_meta.target_directory.parent().unwrap())?,
4479        );
4480
4481        // copy the binary under the test content dir so it can be shared with the guest
4482        let test_content_temp_dir =
4483            PathBuf::from(std::env::var(VMM_TESTS_CONTENT_DIR)?).join(NESTED_TEMP_DIR);
4484        fs_err::create_dir_all(&test_content_temp_dir)?;
4485        let temp_path = tempfile::NamedTempFile::new_in(&test_content_temp_dir)
4486            .context("failed to create temp nested test")?
4487            .into_temp_path();
4488
4489        fs_err::copy(&local_binary_path, &temp_path)?;
4490
4491        Ok(temp_path)
4492    }
4493
4494    fn resolve(&self, discover_artifacts: bool) -> anyhow::Result<ResolvedNestedTestDeps> {
4495        let test_binary = self
4496            .extract_binary()
4497            .context("failed to extract nextest binary")?;
4498
4499        let artifacts = if discover_artifacts {
4500            petri_artifacts_core::query_test_binary_artifacts(&test_binary, &[&self.test_name])
4501                .context("failed to query test binary for artifacts")?
4502                .into_artifacts_list()
4503                .into_iter()
4504                .filter(|id| {
4505                    id != petri_artifacts_common::artifacts::TEST_LOG_DIRECTORY::GLOBAL_UNIQUE_ID
4506                })
4507                .map(|id| {
4508                    let handle = petri_artifacts_core::artifact_from_id(&id)
4509                        .with_context(|| format!("unknown artifact handle: {id}"))?;
4510                    Ok((handle, resolve_artifact(handle)?))
4511                })
4512                .collect::<anyhow::Result<Vec<_>>>()?
4513        } else {
4514            Vec::new()
4515        };
4516
4517        Ok(ResolvedNestedTestDeps {
4518            test_binary,
4519            test_name: self.test_name.clone(),
4520            artifacts,
4521        })
4522    }
4523}
4524
4525trait PetriGuestPaths {
4526    /// Get the OS flavor
4527    fn os_flavor(&self) -> OsFlavor;
4528
4529    /// Get the path separator
4530    fn path_separator(&self) -> &'static str {
4531        match self.os_flavor() {
4532            OsFlavor::Windows => "\\",
4533            _ => "/",
4534        }
4535    }
4536
4537    /// Get the path to the agent disk, if it exists
4538    ///
4539    /// Does not include any subsequent path separators.
4540    fn agent_disk_root(&self) -> &'static str {
4541        match self.os_flavor() {
4542            OsFlavor::Windows => "D:",
4543            _ => "/cidata",
4544        }
4545    }
4546
4547    /// Construct a path with the appropriate path component separators for
4548    /// the guest.
4549    fn build_guest_path(&self, components: &[String]) -> String {
4550        components.join(self.path_separator())
4551    }
4552
4553    /// Construct a path with the appropriate path component separators for
4554    /// the guest from an existing host path.
4555    fn convert_guest_path(&self, path: &Path) -> String {
4556        self.build_guest_path(
4557            &path
4558                .iter()
4559                .map(|c| c.to_str().expect("path not utf8").to_string())
4560                .collect::<Vec<_>>(),
4561        )
4562    }
4563
4564    /// Returns the paths to each of the parent directories.
4565    fn parents<'a>(&self, path: &'a str) -> Vec<&'a str> {
4566        path.match_indices(self.path_separator())
4567            .map(|(x, _)| &path[0..x])
4568            .collect()
4569    }
4570
4571    /// Construct the path to a file in the agent disk.
4572    ///
4573    /// Panics if no agent disk exists.
4574    fn agent_disk_path(&self, components: &[String]) -> String {
4575        self.build_guest_path(&[&[self.agent_disk_root().into()], components].concat())
4576    }
4577
4578    /// Construct a binary name with the appropriate extension for the guest.
4579    fn binary_with_extension(&self, name: &str) -> String {
4580        match self.os_flavor() {
4581            OsFlavor::Windows => format!("{name}.exe"),
4582            _ => name.into(),
4583        }
4584    }
4585}
4586
4587struct FatFsPathBuilder(OsFlavor);
4588
4589impl PetriGuestPaths for FatFsPathBuilder {
4590    fn os_flavor(&self) -> OsFlavor {
4591        self.0
4592    }
4593
4594    fn path_separator(&self) -> &'static str {
4595        "/"
4596    }
4597}
4598
4599impl<T: PetriVmmBackend> PetriGuestPaths for PetriVm<T> {
4600    fn os_flavor(&self) -> OsFlavor {
4601        self.properties.os_flavor
4602    }
4603}
4604
4605impl<T: PetriVmmBackend> PetriGuestPaths for PetriVmBuilder<T> {
4606    fn os_flavor(&self) -> OsFlavor {
4607        self.os_flavor()
4608    }
4609}
4610
4611#[cfg(test)]
4612mod tests {
4613    use super::*;
4614    use crate::Drive;
4615    use crate::VmbusStorageController;
4616    use crate::VmbusStorageType;
4617    use crate::Vtl;
4618
4619    #[derive(Clone, Copy)]
4620    enum TestInspectBehavior {
4621        Success,
4622        Error,
4623        Panic,
4624    }
4625
4626    #[derive(Clone)]
4627    struct TestInspector(TestInspectBehavior);
4628
4629    #[async_trait::async_trait]
4630    impl PetriVmInspector for TestInspector {
4631        async fn inspect(&self, _path: &str) -> anyhow::Result<inspect::Node> {
4632            match self.0 {
4633                TestInspectBehavior::Success => Ok(inspect::Node::Unevaluated),
4634                TestInspectBehavior::Error => anyhow::bail!("inspect failed"),
4635                TestInspectBehavior::Panic => panic!("inspect panicked"),
4636            }
4637        }
4638    }
4639
4640    struct TestFramebuffer;
4641
4642    #[async_trait::async_trait]
4643    impl PetriVmFramebufferAccess for TestFramebuffer {
4644        async fn screenshot(
4645            &mut self,
4646            _image: &mut Vec<u8>,
4647        ) -> anyhow::Result<Option<VmScreenshotMeta>> {
4648            unreachable!()
4649        }
4650    }
4651
4652    struct TestRuntime(TestInspectBehavior);
4653
4654    #[async_trait::async_trait]
4655    impl PetriVmRuntime for TestRuntime {
4656        type VmInspector = TestInspector;
4657        type VmFramebufferAccess = TestFramebuffer;
4658
4659        async fn teardown(self) -> anyhow::Result<()> {
4660            Ok(())
4661        }
4662
4663        async fn wait_for_halt(
4664            &mut self,
4665            _allow_reset: bool,
4666        ) -> anyhow::Result<PetriHaltReasonDetail> {
4667            unreachable!()
4668        }
4669
4670        async fn wait_for_agent(&mut self, _set_high_vtl: bool) -> anyhow::Result<PipetteClient> {
4671            unreachable!()
4672        }
4673
4674        fn openhcl_diag(&self) -> Option<OpenHclDiagHandler> {
4675            None
4676        }
4677
4678        async fn wait_for_boot_event(
4679            &mut self,
4680            _timeout: Option<Duration>,
4681        ) -> anyhow::Result<Option<FirmwareEvent>> {
4682            unreachable!()
4683        }
4684
4685        async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()> {
4686            unreachable!()
4687        }
4688
4689        async fn send_enlightened_shutdown(&mut self, _kind: ShutdownKind) -> anyhow::Result<()> {
4690            unreachable!()
4691        }
4692
4693        async fn restart_openhcl(
4694            &mut self,
4695            _new_openhcl: &ResolvedArtifact,
4696            _flags: OpenHclServicingFlags,
4697        ) -> anyhow::Result<()> {
4698            unreachable!()
4699        }
4700
4701        async fn save_openhcl(
4702            &mut self,
4703            _new_openhcl: &ResolvedArtifact,
4704            _flags: OpenHclServicingFlags,
4705        ) -> anyhow::Result<()> {
4706            unreachable!()
4707        }
4708
4709        async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
4710            unreachable!()
4711        }
4712
4713        async fn update_command_line(&mut self, _command_line: &str) -> anyhow::Result<()> {
4714            unreachable!()
4715        }
4716
4717        fn inspector(&self) -> Option<Self::VmInspector> {
4718            Some(TestInspector(self.0))
4719        }
4720
4721        async fn reset(&mut self) -> anyhow::Result<()> {
4722            unreachable!()
4723        }
4724
4725        async fn set_vtl2_settings(&mut self, _settings: &Vtl2Settings) -> anyhow::Result<()> {
4726            unreachable!()
4727        }
4728
4729        async fn set_vmbus_drive(
4730            &mut self,
4731            _disk: &Drive,
4732            _controller_id: &Guid,
4733            _controller_location: u32,
4734        ) -> anyhow::Result<()> {
4735            unreachable!()
4736        }
4737    }
4738
4739    fn test_runtime_guard(
4740        driver: DefaultDriver,
4741        log_source: &PetriLogSource,
4742        behavior: TestInspectBehavior,
4743    ) -> PetriVmRuntimeGuard<TestRuntime> {
4744        PetriVmRuntimeGuard {
4745            runtime: Some(TestRuntime(behavior)),
4746            driver,
4747            log_source: log_source.clone(),
4748            capture_inspect_on_drop: true,
4749        }
4750    }
4751
4752    fn run_failed_test(
4753        log_source: &PetriLogSource,
4754        behavior: TestInspectBehavior,
4755    ) -> anyhow::Result<()> {
4756        let pool = pal_async::DefaultPool::new();
4757        let driver = pool.driver();
4758        drop(test_runtime_guard(driver.clone(), log_source, behavior));
4759        drop(driver);
4760        pool.run();
4761        anyhow::bail!("original test failure")
4762    }
4763
4764    fn inspect_attachment_count(log_source: &PetriLogSource) -> usize {
4765        fs_err::read_dir(log_source.output_dir())
4766            .unwrap()
4767            .filter_map(Result::ok)
4768            .filter(|entry| {
4769                entry
4770                    .file_name()
4771                    .to_string_lossy()
4772                    .starts_with("failure_inspect_vmm")
4773            })
4774            .count()
4775    }
4776
4777    #[test]
4778    fn test_runtime_guard_failure_capture() {
4779        let temp_dir = tempfile::tempdir().unwrap();
4780        let log_source = crate::tracing::try_init_tracing(
4781            temp_dir.path(),
4782            tracing::level_filters::LevelFilter::DEBUG,
4783            None,
4784        )
4785        .unwrap();
4786
4787        let error = run_failed_test(&log_source, TestInspectBehavior::Success).unwrap_err();
4788        assert_eq!(error.to_string(), "original test failure");
4789        assert_eq!(inspect_attachment_count(&log_source), 1);
4790
4791        let pool = pal_async::DefaultPool::new();
4792        let driver = pool.driver();
4793        let mut guard =
4794            test_runtime_guard(driver.clone(), &log_source, TestInspectBehavior::Success);
4795        let _runtime = guard.take_for_teardown();
4796        drop(guard);
4797        drop(driver);
4798        pool.run();
4799        assert_eq!(inspect_attachment_count(&log_source), 1);
4800
4801        for behavior in [TestInspectBehavior::Error, TestInspectBehavior::Panic] {
4802            let error = run_failed_test(&log_source, behavior).unwrap_err();
4803            assert_eq!(error.to_string(), "original test failure");
4804            assert_eq!(inspect_attachment_count(&log_source), 1);
4805        }
4806    }
4807
4808    #[test]
4809    fn test_vmbus_storage_controller() {
4810        let mut controller = VmbusStorageController::new(Vtl::Vtl0, VmbusStorageType::Scsi);
4811        assert_eq!(
4812            controller.set_drive(Some(1), Drive::new(None, false), false),
4813            1
4814        );
4815        assert!(controller.drives.contains_key(&1));
4816        assert_eq!(
4817            controller.set_drive(None, Drive::new(None, false), false),
4818            0
4819        );
4820        assert!(controller.drives.contains_key(&0));
4821        assert_eq!(
4822            controller.set_drive(None, Drive::new(None, false), false),
4823            2
4824        );
4825        assert!(controller.drives.contains_key(&2));
4826        assert_eq!(
4827            controller.set_drive(Some(0), Drive::new(None, false), true),
4828            0
4829        );
4830        assert!(controller.drives.contains_key(&0));
4831    }
4832}