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;
9pub mod vtl2_settings;
10
11use crate::PetriLogSource;
12use crate::PetriTestParams;
13use crate::ShutdownKind;
14use crate::disk_image::AgentImage;
15use crate::disk_image::SECTOR_SIZE;
16use crate::openhcl_diag::OpenHclDiagHandler;
17use crate::test::PetriPostTestHook;
18use crate::vtl2_settings::ControllerType;
19use crate::vtl2_settings::Vtl2LunBuilder;
20use crate::vtl2_settings::Vtl2StorageBackingDeviceBuilder;
21use crate::vtl2_settings::Vtl2StorageControllerBuilder;
22use async_trait::async_trait;
23use get_resources::ged::FirmwareEvent;
24use guid::Guid;
25use mesh::CancelContext;
26use openvmm_defs::config::Vtl2BaseAddressType;
27use pal_async::DefaultDriver;
28use pal_async::task::Spawn;
29use pal_async::task::Task;
30use pal_async::timer::PolledTimer;
31use petri_artifacts_common::tags::GuestQuirks;
32use petri_artifacts_common::tags::GuestQuirksInner;
33use petri_artifacts_common::tags::InitialRebootCondition;
34use petri_artifacts_common::tags::IsOpenhclIgvm;
35use petri_artifacts_common::tags::IsTestVmgs;
36use petri_artifacts_common::tags::MachineArch;
37use petri_artifacts_common::tags::OsFlavor;
38use petri_artifacts_core::ArtifactResolver;
39use petri_artifacts_core::ArtifactSource;
40use petri_artifacts_core::ResolvedArtifact;
41use petri_artifacts_core::ResolvedArtifactSource;
42use petri_artifacts_core::ResolvedOptionalArtifact;
43use pipette_client::PipetteClient;
44use std::collections::BTreeMap;
45use std::collections::HashMap;
46use std::collections::hash_map::DefaultHasher;
47use std::fmt::Debug;
48use std::hash::Hash;
49use std::hash::Hasher;
50use std::path::Path;
51use std::path::PathBuf;
52use std::sync::Arc;
53use std::time::Duration;
54use tempfile::TempPath;
55use vmgs_resources::GuestStateEncryptionPolicy;
56use vtl2_settings_proto::StorageController;
57use vtl2_settings_proto::Vtl2Settings;
58
59/// The set of artifacts and resources needed to instantiate a
60/// [`PetriVmBuilder`].
61pub struct PetriVmArtifacts<T: PetriVmmBackend> {
62    /// Artifacts needed to launch the host VMM used for the test
63    pub backend: T,
64    /// Firmware and/or OS to load into the VM and associated settings
65    pub firmware: Firmware,
66    /// The architecture of the VM
67    pub arch: MachineArch,
68    /// Agent to run in the guest
69    pub agent_image: Option<AgentImage>,
70    /// Agent to run in OpenHCL
71    pub openhcl_agent_image: Option<AgentImage>,
72    /// Raw pipette binary path (for embedding in initrd via CPIO append)
73    pub pipette_binary: Option<ResolvedArtifact>,
74}
75
76impl<T: PetriVmmBackend> PetriVmArtifacts<T> {
77    /// Resolves the artifacts needed to instantiate a [`PetriVmBuilder`].
78    ///
79    /// Returns `None` if the supplied configuration is not supported on this platform.
80    pub fn new(
81        resolver: &ArtifactResolver<'_>,
82        firmware: Firmware,
83        arch: MachineArch,
84        with_vtl0_pipette: bool,
85    ) -> Option<Self> {
86        if !T::check_compat(&firmware, arch) {
87            return None;
88        }
89
90        let pipette_binary = if with_vtl0_pipette {
91            Some(Self::resolve_pipette_binary(
92                resolver,
93                firmware.os_flavor(),
94                arch,
95            ))
96        } else {
97            None
98        };
99
100        Some(Self {
101            backend: T::new(resolver),
102            arch,
103            agent_image: Some(if with_vtl0_pipette {
104                AgentImage::new(firmware.os_flavor()).with_pipette(resolver, arch)
105            } else {
106                AgentImage::new(firmware.os_flavor())
107            }),
108            openhcl_agent_image: if firmware.is_openhcl() {
109                Some(AgentImage::new(OsFlavor::Linux).with_pipette(resolver, arch))
110            } else {
111                None
112            },
113            pipette_binary,
114            firmware,
115        })
116    }
117
118    fn resolve_pipette_binary(
119        resolver: &ArtifactResolver<'_>,
120        os_flavor: OsFlavor,
121        arch: MachineArch,
122    ) -> ResolvedArtifact {
123        use petri_artifacts_common::artifacts as common_artifacts;
124        match (os_flavor, arch) {
125            (OsFlavor::Linux, MachineArch::X86_64) => resolver
126                .require(common_artifacts::PIPETTE_LINUX_X64)
127                .erase(),
128            (OsFlavor::Linux, MachineArch::Aarch64) => resolver
129                .require(common_artifacts::PIPETTE_LINUX_AARCH64)
130                .erase(),
131            (OsFlavor::Windows, MachineArch::X86_64) => resolver
132                .require(common_artifacts::PIPETTE_WINDOWS_X64)
133                .erase(),
134            (OsFlavor::Windows, MachineArch::Aarch64) => resolver
135                .require(common_artifacts::PIPETTE_WINDOWS_AARCH64)
136                .erase(),
137            (OsFlavor::FreeBsd | OsFlavor::Uefi, _) => {
138                panic!("No pipette binary for this OS flavor")
139            }
140        }
141    }
142}
143
144/// Petri VM builder
145pub struct PetriVmBuilder<T: PetriVmmBackend> {
146    /// Artifacts needed to launch the host VMM used for the test
147    backend: T,
148    /// VM configuration
149    config: PetriVmConfig,
150    /// Function to modify the VMM-specific configuration
151    modify_vmm_config: Option<ModifyFn<T::VmmConfig>>,
152    /// VMM-agnostic resources
153    resources: PetriVmResources,
154
155    // VMM-specific quirks for the configured firmware
156    guest_quirks: GuestQuirksInner,
157    vmm_quirks: VmmQuirks,
158
159    // Test-specific boot behavior expectations.
160    // Defaults to expected behavior for firmware configuration.
161    expected_boot_event: Option<FirmwareEvent>,
162    override_expect_reset: bool,
163
164    // Config that is used to modify the `PetriVmConfig` before it is passed
165    // to the VMM backend.
166    /// Agent to run in the guest
167    agent_image: Option<AgentImage>,
168    /// Agent to run in OpenHCL
169    openhcl_agent_image: Option<AgentImage>,
170    /// The boot device type for the VM
171    boot_device_type: BootDeviceType,
172    /// Override for the PCIe root port the boot NVMe controller is placed on
173    /// when [`BootDeviceType::PcieNvme`] is used. Defaults to `s0rc0rp0`.
174    pcie_boot_port: Option<String>,
175
176    // Minimal mode: skip default devices, serial, save/restore.
177    minimal_mode: bool,
178    // Raw pipette binary path (for CPIO embedding in initrd).
179    pipette_binary: Option<ResolvedArtifact>,
180    // Enable serial output even in minimal mode (for diagnostics).
181    enable_serial: bool,
182    // Enable periodic framebuffer screenshots.
183    enable_screenshots: bool,
184    // Pre-built initrd with pipette already injected (skips runtime injection).
185    prebuilt_initrd: Option<PathBuf>,
186    // Use virtio vsock instead of VMBus-based hvsocket for guest communication.
187    use_virtio_vsock: bool,
188    // Use the Linux kernel vhost-vsock backend with this guest CID.
189    #[cfg(target_os = "linux")]
190    vhost_vsock_guest_cid: Option<u32>,
191    // Disable VMBus entirely (no vmbus server, no vmbus storage controllers).
192    no_vmbus: bool,
193    // Disable the hypervisor (HV#1) enlightenments. Implies `no_vmbus`.
194    no_hv: bool,
195}
196
197impl<T: PetriVmmBackend> Debug for PetriVmBuilder<T> {
198    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199        f.debug_struct("PetriVmBuilder")
200            .field("backend", &self.backend)
201            .field("config", &self.config)
202            .field("modify_vmm_config", &self.modify_vmm_config.is_some())
203            .field("resources", &self.resources)
204            .field("guest_quirks", &self.guest_quirks)
205            .field("vmm_quirks", &self.vmm_quirks)
206            .field("expected_boot_event", &self.expected_boot_event)
207            .field("override_expect_reset", &self.override_expect_reset)
208            .field("agent_image", &self.agent_image)
209            .field("openhcl_agent_image", &self.openhcl_agent_image)
210            .field("boot_device_type", &self.boot_device_type)
211            .field("pcie_boot_port", &self.pcie_boot_port)
212            .field("minimal_mode", &self.minimal_mode)
213            .field("enable_serial", &self.enable_serial)
214            .field("enable_screenshots", &self.enable_screenshots)
215            .field("prebuilt_initrd", &self.prebuilt_initrd)
216            .field("use_virtio_vsock", &self.use_virtio_vsock)
217            .field("no_vmbus", &self.no_vmbus)
218            .field("no_hv", &self.no_hv)
219            .finish()
220    }
221}
222
223/// Petri VM configuration
224#[derive(Debug)]
225pub struct PetriVmConfig {
226    /// The name of the VM
227    pub name: String,
228    /// The architecture of the VM
229    pub arch: MachineArch,
230    /// Log levels for the host VMM process.
231    pub host_log_levels: Option<OpenvmmLogConfig>,
232    /// Firmware and/or OS to load into the VM and associated settings
233    pub firmware: Firmware,
234    /// Whether to enable guest hibernation support.
235    pub hibernation_enabled: bool,
236    /// Whether to expose an IPMI KCS interface to the guest.
237    pub ipmi_enabled: bool,
238    /// The amount of memory, in bytes, to assign to the VM
239    pub memory: MemoryConfig,
240    /// The processor topology for the VM
241    pub proc_topology: ProcessorTopology,
242    /// VM guest state
243    pub vmgs: PetriVmgsResource,
244    /// TPM configuration
245    pub tpm: Option<TpmConfig>,
246    /// Storage controllers and associated disks
247    pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
248    /// PCIe NVMe drives.
249    pub pcie_nvme_drives: Vec<PcieNvmeDrive>,
250    /// PCIe virtio-blk drives.
251    pub pcie_virtio_blk_drives: Vec<PcieVirtioBlkDrive>,
252    /// Physical NVMe devices to attach
253    pub physical_nvme_devices: HashMap<Guid, PhysicalNvmeDevice>,
254}
255
256/// PCIe NVMe drive configuration.
257#[derive(Debug)]
258pub struct PcieNvmeDrive {
259    /// PCIe root port name (e.g. "s0rc0rp0").
260    pub port_name: String,
261    /// NVMe namespace ID.
262    pub nsid: u32,
263    /// The drive to attach.
264    pub drive: Drive,
265}
266
267/// PCIe virtio-blk drive configuration.
268#[derive(Debug)]
269pub struct PcieVirtioBlkDrive {
270    /// PCIe root port name (e.g. "s0rc0rp0").
271    pub port_name: String,
272    /// The drive to attach.
273    pub drive: Drive,
274}
275
276/// Physical NVMe device to assign to a VM.
277/// Only used in closed-source HyperV tests
278#[derive(Debug, Clone)]
279pub struct PhysicalNvmeDevice {
280    /// The VTL to assign the physical NVMe device to.
281    pub target_vtl: Vtl,
282    /// NVMe namespace ID.
283    pub nsid: u32,
284    /// Namespace size in MiB
285    pub namespace_size_mib: u64,
286}
287
288/// Static properties about the VM for convenience during contruction and
289/// runtime of a VMM backend
290pub struct PetriVmProperties {
291    /// Whether this VM uses OpenHCL
292    pub is_openhcl: bool,
293    /// Whether this VM is isolated
294    pub is_isolated: bool,
295    /// Whether this VM uses the PCAT BIOS
296    pub is_pcat: bool,
297    /// Whether this VM boots with linux direct
298    pub is_linux_direct: bool,
299    /// Whether this VM is using pipette in VTL0
300    pub using_vtl0_pipette: bool,
301    /// Whether this VM is using VPCI
302    pub using_vpci: bool,
303    /// The OS flavor of the guest in the VM
304    pub os_flavor: OsFlavor,
305    /// Minimal mode: skip default devices, serial, save/restore
306    pub minimal_mode: bool,
307    /// Pipette embeds in initrd as PID 1 (non-OpenHCL Linux direct boot)
308    pub uses_pipette_as_init: bool,
309    /// Enable serial output even in minimal mode
310    pub enable_serial: bool,
311    /// Pre-built initrd path with pipette already injected
312    pub prebuilt_initrd: Option<PathBuf>,
313    /// Whether the VM has a CIDATA agent disk attached
314    pub has_agent_disk: bool,
315    /// Use virtio vsock instead of VMBus-based hvsocket
316    pub use_virtio_vsock: bool,
317    /// Linux kernel vhost-vsock guest CID, when that backend is enabled.
318    #[cfg(target_os = "linux")]
319    pub vhost_vsock_guest_cid: Option<u32>,
320    /// VMBus is entirely disabled
321    pub no_vmbus: bool,
322    /// The hypervisor (HV#1) enlightenments are entirely disabled
323    pub no_hv: bool,
324}
325
326/// VM configuration that can be changed after the VM is created
327pub struct PetriVmRuntimeConfig {
328    /// VTL2 settings
329    pub vtl2_settings: Option<Vtl2Settings>,
330    /// IDE controllers and associated disks
331    pub ide_controllers: Option<[[Option<Drive>; 2]; 2]>,
332    /// Storage controllers and associated disks
333    pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
334}
335
336/// Resources used by a Petri VM during contruction and runtime
337#[derive(Debug)]
338pub struct PetriVmResources {
339    driver: DefaultDriver,
340    log_source: PetriLogSource,
341}
342
343/// Trait for VMM-specific contruction and runtime resources
344#[async_trait]
345pub trait PetriVmmBackend: Debug {
346    /// VMM-specific configuration
347    type VmmConfig;
348
349    /// Runtime object
350    type VmRuntime: PetriVmRuntime;
351
352    /// Check whether the combination of firmware and architecture is
353    /// supported on the VMM.
354    fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool;
355
356    /// Select backend specific quirks guest and vmm quirks.
357    fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks);
358
359    /// Get the default servicing flags (based on what this backend supports)
360    fn default_servicing_flags() -> OpenHclServicingFlags;
361
362    /// Create a disk for guest crash dumps, and a post-test hook to open the disk
363    /// to allow for reading the dumps.
364    fn create_guest_dump_disk() -> anyhow::Result<
365        Option<(
366            Arc<TempPath>,
367            Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
368        )>,
369    >;
370
371    /// Resolve any artifacts needed to use this backend
372    fn new(resolver: &ArtifactResolver<'_>) -> Self;
373
374    /// Create and start VM from the generic config using the VMM backend
375    async fn run(
376        self,
377        config: PetriVmConfig,
378        modify_vmm_config: Option<ModifyFn<Self::VmmConfig>>,
379        resources: &PetriVmResources,
380        properties: PetriVmProperties,
381    ) -> anyhow::Result<(Self::VmRuntime, PetriVmRuntimeConfig)>;
382}
383
384// IDE is only ever offered to VTL0
385pub(crate) const PETRI_IDE_BOOT_CONTROLLER_NUMBER: u32 = 0;
386pub(crate) const PETRI_IDE_BOOT_LUN: u8 = 0;
387pub(crate) const PETRI_IDE_BOOT_CONTROLLER: Guid =
388    guid::guid!("ca56751f-e643-4bef-bf54-f73678e8b7b5");
389
390// SCSI luns used for both VTL0 and VTL2
391pub(crate) const PETRI_SCSI_BOOT_LUN: u32 = 0;
392pub(crate) const PETRI_SCSI_PIPETTE_LUN: u32 = 1;
393pub(crate) const PETRI_SCSI_CRASH_LUN: u32 = 2;
394/// VTL0 SCSI controller instance guid used by Petri
395pub(crate) const PETRI_SCSI_VTL0_CONTROLLER: Guid =
396    guid::guid!("27b553e8-8b39-411b-a55f-839971a7884f");
397/// VTL2 SCSI controller instance guid used by Petri
398pub(crate) const PETRI_SCSI_VTL2_CONTROLLER: Guid =
399    guid::guid!("766e96f8-2ceb-437e-afe3-a93169e48a7c");
400/// SCSI controller instance guid offered to VTL0 by VTL2
401pub(crate) const PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER: Guid =
402    guid::guid!("6c474f47-ed39-49e6-bbb9-142177a1da6e");
403
404/// The namespace ID used by Petri for the boot disk
405pub(crate) const PETRI_NVME_BOOT_NSID: u32 = 37;
406/// VTL0 NVMe controller instance guid used by Petri
407pub(crate) const PETRI_NVME_BOOT_VTL0_CONTROLLER: Guid =
408    guid::guid!("e23a04e2-90f5-4852-bc9d-e7ac691b756c");
409/// VTL2 NVMe controller instance guid used by Petri
410pub(crate) const PETRI_NVME_BOOT_VTL2_CONTROLLER: Guid =
411    guid::guid!("92bc8346-718b-449a-8751-edbf3dcd27e4");
412
413/// PCIe root port used by Petri for the agent/cidata disk (no-vmbus mode)
414pub(crate) const PETRI_PCIE_NVME_AGENT_PORT: &str = "s0rc0rp1";
415/// NVMe namespace ID used by Petri for the agent/cidata disk (no-vmbus mode)
416pub(crate) const PETRI_PCIE_NVME_AGENT_NSID: u32 = 1;
417
418/// A constructed Petri VM
419pub struct PetriVm<T: PetriVmmBackend> {
420    resources: PetriVmResources,
421    runtime: T::VmRuntime,
422    watchdog_tasks: Vec<Task<()>>,
423    openhcl_diag_handler: Option<OpenHclDiagHandler>,
424
425    arch: MachineArch,
426    guest_quirks: GuestQuirksInner,
427    vmm_quirks: VmmQuirks,
428    expected_boot_event: Option<FirmwareEvent>,
429
430    config: PetriVmRuntimeConfig,
431}
432
433impl<T: PetriVmmBackend> PetriVmBuilder<T> {
434    /// Create a new VM configuration.
435    pub fn new(
436        params: PetriTestParams<'_>,
437        artifacts: PetriVmArtifacts<T>,
438        driver: &DefaultDriver,
439    ) -> anyhow::Result<Self> {
440        let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
441        let expected_boot_event = artifacts.firmware.expected_boot_event();
442        let boot_device_type = match artifacts.firmware {
443            Firmware::LinuxDirect { .. } => BootDeviceType::None,
444            Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
445            Firmware::Pcat { .. } => BootDeviceType::Ide,
446            Firmware::OpenhclPcat { .. } => BootDeviceType::IdeViaScsi,
447            Firmware::Uefi {
448                guest: UefiGuest::None,
449                ..
450            }
451            | Firmware::OpenhclUefi {
452                guest: UefiGuest::None,
453                ..
454            } => BootDeviceType::None,
455            Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
456        };
457
458        Ok(Self {
459            backend: artifacts.backend,
460            config: PetriVmConfig {
461                name: make_vm_safe_name(params.test_name),
462                arch: artifacts.arch,
463                host_log_levels: None,
464                firmware: artifacts.firmware,
465                hibernation_enabled: false,
466                ipmi_enabled: false,
467                memory: Default::default(),
468                proc_topology: Default::default(),
469
470                vmgs: PetriVmgsResource::Ephemeral,
471                tpm: None,
472                vmbus_storage_controllers: HashMap::new(),
473                pcie_nvme_drives: Vec::new(),
474                pcie_virtio_blk_drives: Vec::new(),
475                physical_nvme_devices: HashMap::new(),
476            },
477            modify_vmm_config: None,
478            resources: PetriVmResources {
479                driver: driver.clone(),
480                log_source: params.logger.clone(),
481            },
482
483            guest_quirks,
484            vmm_quirks,
485            expected_boot_event,
486            override_expect_reset: false,
487
488            agent_image: artifacts.agent_image,
489            openhcl_agent_image: artifacts.openhcl_agent_image,
490            boot_device_type,
491            pcie_boot_port: None,
492
493            minimal_mode: false,
494            pipette_binary: artifacts.pipette_binary,
495            enable_serial: true,
496            enable_screenshots: true,
497            prebuilt_initrd: None,
498            use_virtio_vsock: false,
499            #[cfg(target_os = "linux")]
500            vhost_vsock_guest_cid: None,
501            no_vmbus: false,
502            no_hv: false,
503        }
504        .add_petri_scsi_controllers()
505        .add_guest_crash_disk(params.post_test_hooks))
506    }
507
508    /// Create a minimal VM builder with only the bare minimum device set.
509    ///
510    /// Unlike [`new()`](Self::new), this constructor:
511    /// - Does not add default VMBus devices (shutdown IC, KVP, etc.)
512    /// - Does not add serial ports
513    /// - Does not add SCSI controllers or crash dump disks
514    /// - Does not verify save/restore on boot
515    ///
516    /// Use builder methods to opt in to specific devices. Intended for
517    /// performance tests where minimal overhead is critical.
518    pub fn minimal(
519        params: PetriTestParams<'_>,
520        artifacts: PetriVmArtifacts<T>,
521        driver: &DefaultDriver,
522    ) -> anyhow::Result<Self> {
523        let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
524        let expected_boot_event = artifacts.firmware.expected_boot_event();
525        let boot_device_type = match artifacts.firmware {
526            Firmware::LinuxDirect { .. } => BootDeviceType::None,
527            Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
528            Firmware::Pcat { .. } => BootDeviceType::Ide,
529            Firmware::OpenhclPcat { .. } => BootDeviceType::IdeViaScsi,
530            Firmware::Uefi {
531                guest: UefiGuest::None,
532                ..
533            }
534            | Firmware::OpenhclUefi {
535                guest: UefiGuest::None,
536                ..
537            } => BootDeviceType::None,
538            Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
539        };
540
541        Ok(Self {
542            backend: artifacts.backend,
543            config: PetriVmConfig {
544                name: make_vm_safe_name(params.test_name),
545                arch: artifacts.arch,
546                host_log_levels: None,
547                firmware: artifacts.firmware,
548                hibernation_enabled: false,
549                ipmi_enabled: false,
550                memory: Default::default(),
551                proc_topology: Default::default(),
552
553                vmgs: PetriVmgsResource::Ephemeral,
554                tpm: None,
555                vmbus_storage_controllers: HashMap::new(),
556                pcie_nvme_drives: Vec::new(),
557                pcie_virtio_blk_drives: Vec::new(),
558                physical_nvme_devices: HashMap::new(),
559            },
560            modify_vmm_config: None,
561            resources: PetriVmResources {
562                driver: driver.clone(),
563                log_source: params.logger.clone(),
564            },
565
566            guest_quirks,
567            vmm_quirks,
568            expected_boot_event,
569            override_expect_reset: false,
570
571            agent_image: artifacts.agent_image,
572            openhcl_agent_image: artifacts.openhcl_agent_image,
573            boot_device_type,
574            pcie_boot_port: None,
575
576            minimal_mode: true,
577            pipette_binary: artifacts.pipette_binary,
578            enable_serial: false,
579            enable_screenshots: true,
580            prebuilt_initrd: None,
581            use_virtio_vsock: false,
582            #[cfg(target_os = "linux")]
583            vhost_vsock_guest_cid: None,
584            no_vmbus: false,
585            no_hv: false,
586        })
587    }
588
589    /// Whether this builder is in minimal mode.
590    pub fn is_minimal(&self) -> bool {
591        self.minimal_mode
592    }
593
594    /// Supply a pre-built initrd with pipette already injected.
595    ///
596    /// When set, the builder skips the runtime gzip decompress/inject/
597    /// recompress cycle, using this initrd directly. Use
598    /// [`prepare_initrd`](Self::prepare_initrd) to build the initrd
599    /// ahead of time.
600    pub fn with_prebuilt_initrd(mut self, path: PathBuf) -> Self {
601        self.prebuilt_initrd = Some(path);
602        self
603    }
604
605    /// Pre-build the modified initrd with pipette injected.
606    ///
607    /// Reads the original initrd from the firmware artifacts, injects
608    /// the pipette binary via CPIO, and writes the result to a temp file.
609    /// Returns the path to the temp file. The caller must keep the
610    /// `TempPath` alive until after the VM boots.
611    ///
612    /// Call this once before timing, then pass the path to
613    /// [`with_prebuilt_initrd`](Self::with_prebuilt_initrd) for each
614    /// iteration.
615    pub fn prepare_initrd(&self) -> anyhow::Result<TempPath> {
616        use anyhow::Context;
617        use std::io::Write;
618
619        let initrd_path = self
620            .config
621            .firmware
622            .linux_direct_initrd()
623            .context("prepare_initrd requires Linux direct boot with initrd")?;
624        let pipette_path = self
625            .pipette_binary
626            .as_ref()
627            .context("prepare_initrd requires a pipette binary")?;
628
629        let initrd_gz = std::fs::read(initrd_path)
630            .with_context(|| format!("failed to read initrd at {}", initrd_path.display()))?;
631        let pipette_data = std::fs::read(pipette_path.get()).with_context(|| {
632            format!(
633                "failed to read pipette binary at {}",
634                pipette_path.get().display()
635            )
636        })?;
637
638        let merged_gz =
639            initrd_cpio::inject_into_initrd(&initrd_gz, "pipette", &pipette_data, 0o100755)
640                .context("failed to inject pipette into initrd")?;
641
642        let mut tmp = tempfile::NamedTempFile::new()
643            .context("failed to create temp file for pre-built initrd")?;
644        tmp.write_all(&merged_gz)
645            .context("failed to write pre-built initrd")?;
646
647        Ok(tmp.into_temp_path())
648    }
649
650    /// Enable serial port output even in minimal mode.
651    ///
652    /// Useful for diagnostics — the serial device overhead is negligible;
653    /// the cost comes from kernel console output, which is controlled via
654    /// the kernel cmdline (`quiet loglevel=0`).
655    ///
656    /// Note: this currently only affects LinuxDirect boot (kernel cmdline
657    /// and emulated serial backends). UEFI paths are unaffected.
658    pub fn with_serial_output(mut self) -> Self {
659        self.enable_serial = true;
660        self
661    }
662
663    /// Disable serial port output.
664    ///
665    /// Suppresses serial device creation, eliminating the `[uefi]` / `[openhcl]`
666    /// log lines. Useful for performance tests where serial noise is unwanted.
667    pub fn without_serial_output(mut self) -> Self {
668        self.enable_serial = false;
669        self
670    }
671
672    /// Disable periodic framebuffer screenshots.
673    ///
674    /// Suppresses the watchdog task that takes screenshots every 2 seconds,
675    /// eliminating the "No change in framebuffer" debug log lines.
676    pub fn without_screenshots(mut self) -> Self {
677        self.enable_screenshots = false;
678        self
679    }
680
681    /// Use virtio vsock instead of VMBus-based hvsocket for guest communication.
682    /// The virtio-vsock device will use PCIe, so a PCIe root topology must be
683    /// configured.
684    ///
685    /// When enabled, a virtio-vsock device is added to the VM. This device uses
686    /// the same Unix socket relay path that hvsocket would otherwise use, so
687    /// pipette will connect using this.
688    ///
689    /// For Linux direct boot, this also adjusts the kernel command line to
690    /// blacklist hv_sock instead of virtio_vsock.
691    pub fn with_virtio_vsock(mut self) -> Self {
692        self.use_virtio_vsock = true;
693        #[cfg(target_os = "linux")]
694        {
695            self.vhost_vsock_guest_cid = None;
696        }
697        self
698    }
699
700    /// Use the Linux kernel vhost-vsock backend for guest communication.
701    ///
702    /// The host connects directly to the guest's `AF_VSOCK` listener at
703    /// `guest_cid`. The OpenVMM backend automatically uses shared guest memory,
704    /// which is required by kernel vhost.
705    #[cfg(target_os = "linux")]
706    pub fn with_vhost_vsock(mut self, guest_cid: u32) -> Self {
707        assert!(
708            (3..u32::MAX).contains(&guest_cid),
709            "vhost-vsock guest CID must be between 3 and {}",
710            u32::MAX - 1
711        );
712        self.use_virtio_vsock = true;
713        self.vhost_vsock_guest_cid = Some(guest_cid);
714        self
715    }
716
717    /// Disable VMBus entirely.
718    ///
719    /// This removes all VMBus storage controllers. For Linux guests,
720    /// virtio-vsock is used for pipette communication. For Windows guests,
721    /// the caller must also configure TCP pipette transport via
722    /// `modify_backend(|b| b.with_tcp_pipette_nic(port, mac_address))`. The
723    /// guest must boot from a non-VMBus device (e.g. PCIe NVMe).
724    pub fn with_no_vmbus(mut self) -> Self {
725        self.no_vmbus = true;
726        if self.config.firmware.os_flavor() != OsFlavor::Windows {
727            self.use_virtio_vsock = true;
728        }
729        self.config.vmbus_storage_controllers.clear();
730        self
731    }
732
733    /// Disable the hypervisor (HV#1) enlightenments.
734    ///
735    /// This also disables VMBus, since VMBus depends on the hypervisor. On
736    /// aarch64 UEFI this causes the loader to pass the generic SEC platform
737    /// type to the firmware. This mode is not supported on x86_64 UEFI.
738    pub fn with_no_hv(mut self) -> Self {
739        self.no_hv = true;
740        self.with_no_vmbus()
741    }
742
743    fn add_petri_scsi_controllers(self) -> Self {
744        let builder = self.add_vmbus_storage_controller(
745            &PETRI_SCSI_VTL0_CONTROLLER,
746            Vtl::Vtl0,
747            VmbusStorageType::Scsi,
748        );
749
750        if builder.is_openhcl() {
751            builder.add_vmbus_storage_controller(
752                &PETRI_SCSI_VTL2_CONTROLLER,
753                Vtl::Vtl2,
754                VmbusStorageType::Scsi,
755            )
756        } else {
757            builder
758        }
759    }
760
761    fn add_guest_crash_disk(self, post_test_hooks: &mut Vec<PetriPostTestHook>) -> Self {
762        let logger = self.resources.log_source.clone();
763        let (disk, disk_hook) = matches!(
764            self.config.firmware.os_flavor(),
765            OsFlavor::Windows | OsFlavor::Linux
766        )
767        .then(|| T::create_guest_dump_disk().expect("failed to create guest dump disk"))
768        .flatten()
769        .unzip();
770
771        if let Some(disk_hook) = disk_hook {
772            post_test_hooks.push(PetriPostTestHook::new(
773                "extract guest crash dumps".into(),
774                move |test_passed| {
775                    if test_passed {
776                        return Ok(());
777                    }
778                    let mut disk = disk_hook()?;
779                    let gpt = gptman::GPT::read_from(&mut disk, SECTOR_SIZE)?;
780                    let partition = fscommon::StreamSlice::new(
781                        &mut disk,
782                        gpt[1].starting_lba * SECTOR_SIZE,
783                        gpt[1].ending_lba * SECTOR_SIZE,
784                    )?;
785                    let fs = fatfs::FileSystem::new(partition, fatfs::FsOptions::new())?;
786                    for entry in fs.root_dir().iter() {
787                        let Ok(entry) = entry else {
788                            tracing::warn!(?entry, "failed to read entry in guest crash dump disk");
789                            continue;
790                        };
791                        if !entry.is_file() {
792                            tracing::warn!(
793                                ?entry,
794                                "skipping non-file entry in guest crash dump disk"
795                            );
796                            continue;
797                        }
798                        logger.write_attachment(&entry.file_name(), entry.to_file())?;
799                    }
800                    Ok(())
801                },
802            ));
803        }
804
805        if let Some(disk) = disk {
806            self.add_vmbus_drive(
807                Drive::new(Some(Disk::Temporary(disk)), false),
808                &PETRI_SCSI_VTL0_CONTROLLER,
809                Some(PETRI_SCSI_CRASH_LUN),
810            )
811        } else {
812            self
813        }
814    }
815
816    fn add_agent_disks(self) -> Self {
817        self.add_agent_disk_inner(Vtl::Vtl0)
818            .add_agent_disk_inner(Vtl::Vtl2)
819    }
820
821    fn add_agent_disk_inner(mut self, target_vtl: Vtl) -> Self {
822        let (agent_image, controller_id) = match target_vtl {
823            Vtl::Vtl0 => (self.agent_image.as_ref(), PETRI_SCSI_VTL0_CONTROLLER),
824            Vtl::Vtl1 => panic!("no VTL1 agent disk"),
825            Vtl::Vtl2 => (
826                self.openhcl_agent_image.as_ref(),
827                PETRI_SCSI_VTL2_CONTROLLER,
828            ),
829        };
830
831        // When using pipette-as-init, the VTL0 agent disk is only needed
832        // if it carries extra files (pipette itself is in the initrd).
833        if target_vtl == Vtl::Vtl0
834            && self.uses_pipette_as_init()
835            && !agent_image.is_some_and(|i| i.has_extras())
836        {
837            return self;
838        }
839
840        let Some(agent_disk) = agent_image.and_then(|i| {
841            i.build(crate::disk_image::ImageType::Vhd)
842                .expect("failed to build agent image")
843        }) else {
844            return self;
845        };
846
847        // When VMBus is disabled, route the agent disk through PCIe NVMe
848        // instead of VMBus SCSI.
849        if self.no_vmbus {
850            self.config.pcie_nvme_drives.push(PcieNvmeDrive {
851                port_name: PETRI_PCIE_NVME_AGENT_PORT.into(),
852                nsid: PETRI_PCIE_NVME_AGENT_NSID,
853                drive: Drive::new(
854                    Some(Disk::Temporary(Arc::new(agent_disk.into_temp_path()))),
855                    false,
856                ),
857            });
858            return self;
859        }
860
861        // Ensure the storage controller exists (minimal mode doesn't
862        // add controllers upfront).
863        if !self
864            .config
865            .vmbus_storage_controllers
866            .contains_key(&controller_id)
867        {
868            self = self.add_vmbus_storage_controller(
869                &controller_id,
870                target_vtl,
871                VmbusStorageType::Scsi,
872            );
873        }
874
875        self.add_vmbus_drive(
876            Drive::new(
877                Some(Disk::Temporary(Arc::new(agent_disk.into_temp_path()))),
878                false,
879            ),
880            &controller_id,
881            Some(PETRI_SCSI_PIPETTE_LUN),
882        )
883    }
884
885    fn add_boot_disk(mut self) -> Self {
886        if self.boot_device_type.requires_vtl2() && !self.is_openhcl() {
887            panic!("boot device type {:?} requires vtl2", self.boot_device_type);
888        }
889
890        if self.no_vmbus && self.boot_device_type.requires_vmbus() {
891            panic!(
892                "boot device type {:?} requires vmbus, but vmbus is disabled; \
893                 use with_boot_device_type(BootDeviceType::PcieNvme) or similar",
894                self.boot_device_type
895            );
896        }
897
898        if self.boot_device_type.requires_vpci_boot() {
899            self.config
900                .firmware
901                .uefi_config_mut()
902                .expect("vpci boot requires uefi")
903                .enable_vpci_boot = true;
904        }
905
906        if let Some(boot_drive) = self.config.firmware.boot_drive() {
907            match self.boot_device_type {
908                BootDeviceType::None => unreachable!(),
909                BootDeviceType::Ide => self.add_ide_drive(
910                    boot_drive,
911                    PETRI_IDE_BOOT_CONTROLLER_NUMBER,
912                    PETRI_IDE_BOOT_LUN,
913                ),
914                BootDeviceType::IdeViaScsi => self
915                    .add_vmbus_drive(
916                        boot_drive,
917                        &PETRI_SCSI_VTL2_CONTROLLER,
918                        Some(PETRI_SCSI_BOOT_LUN),
919                    )
920                    .add_vtl2_storage_controller(
921                        Vtl2StorageControllerBuilder::new(ControllerType::Ide)
922                            .with_instance_id(PETRI_IDE_BOOT_CONTROLLER)
923                            .add_lun(
924                                Vtl2LunBuilder::disk()
925                                    .with_channel(PETRI_IDE_BOOT_CONTROLLER_NUMBER)
926                                    .with_location(PETRI_IDE_BOOT_LUN as u32)
927                                    .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
928                                        ControllerType::Scsi,
929                                        PETRI_SCSI_VTL2_CONTROLLER,
930                                        PETRI_SCSI_BOOT_LUN,
931                                    )),
932                            )
933                            .build(),
934                    ),
935                BootDeviceType::IdeViaNvme => todo!(),
936                BootDeviceType::Scsi => self.add_vmbus_drive(
937                    boot_drive,
938                    &PETRI_SCSI_VTL0_CONTROLLER,
939                    Some(PETRI_SCSI_BOOT_LUN),
940                ),
941                BootDeviceType::ScsiViaScsi => self
942                    .add_vmbus_drive(
943                        boot_drive,
944                        &PETRI_SCSI_VTL2_CONTROLLER,
945                        Some(PETRI_SCSI_BOOT_LUN),
946                    )
947                    .add_vtl2_storage_controller(
948                        Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
949                            .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
950                            .add_lun(
951                                Vtl2LunBuilder::disk()
952                                    .with_location(PETRI_SCSI_BOOT_LUN)
953                                    .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
954                                        ControllerType::Scsi,
955                                        PETRI_SCSI_VTL2_CONTROLLER,
956                                        PETRI_SCSI_BOOT_LUN,
957                                    )),
958                            )
959                            .build(),
960                    ),
961                BootDeviceType::ScsiViaNvme => self
962                    .add_vmbus_storage_controller(
963                        &PETRI_NVME_BOOT_VTL2_CONTROLLER,
964                        Vtl::Vtl2,
965                        VmbusStorageType::Nvme,
966                    )
967                    .add_vmbus_drive(
968                        boot_drive,
969                        &PETRI_NVME_BOOT_VTL2_CONTROLLER,
970                        Some(PETRI_NVME_BOOT_NSID),
971                    )
972                    .add_vtl2_storage_controller(
973                        Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
974                            .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
975                            .add_lun(
976                                Vtl2LunBuilder::disk()
977                                    .with_location(PETRI_SCSI_BOOT_LUN)
978                                    .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
979                                        ControllerType::Nvme,
980                                        PETRI_NVME_BOOT_VTL2_CONTROLLER,
981                                        PETRI_NVME_BOOT_NSID,
982                                    )),
983                            )
984                            .build(),
985                    ),
986                BootDeviceType::Nvme => self
987                    .add_vmbus_storage_controller(
988                        &PETRI_NVME_BOOT_VTL0_CONTROLLER,
989                        Vtl::Vtl0,
990                        VmbusStorageType::Nvme,
991                    )
992                    .add_vmbus_drive(
993                        boot_drive,
994                        &PETRI_NVME_BOOT_VTL0_CONTROLLER,
995                        Some(PETRI_NVME_BOOT_NSID),
996                    ),
997                BootDeviceType::NvmeViaScsi => todo!(),
998                BootDeviceType::NvmeViaNvme => todo!(),
999                BootDeviceType::PcieNvme => {
1000                    let port_name = self
1001                        .pcie_boot_port
1002                        .clone()
1003                        .unwrap_or_else(|| "s0rc0rp0".into());
1004                    self.config.pcie_nvme_drives.push(PcieNvmeDrive {
1005                        port_name,
1006                        nsid: 1,
1007                        drive: boot_drive,
1008                    });
1009                    self
1010                }
1011                BootDeviceType::PcieVirtioBlk => {
1012                    self.config.pcie_virtio_blk_drives.push(PcieVirtioBlkDrive {
1013                        port_name: "s0rc0rp0".into(),
1014                        drive: boot_drive,
1015                    });
1016                    self
1017                }
1018            }
1019        } else {
1020            self
1021        }
1022    }
1023
1024    /// Whether the VTL0 agent disk will actually be added.
1025    ///
1026    /// False when using pipette-as-init with no extra files (pipette is
1027    /// in the initrd, so the CIDATA disk isn't needed).
1028    fn has_agent_disk(&self) -> bool {
1029        if self.uses_pipette_as_init() {
1030            self.agent_image.as_ref().is_some_and(|i| i.has_extras())
1031        } else {
1032            self.agent_image.is_some()
1033        }
1034    }
1035
1036    /// Get properties about the vm for convenience
1037    pub fn properties(&self) -> PetriVmProperties {
1038        PetriVmProperties {
1039            is_openhcl: self.config.firmware.is_openhcl(),
1040            is_isolated: self.config.firmware.isolation().is_some(),
1041            is_pcat: self.config.firmware.is_pcat(),
1042            is_linux_direct: self.config.firmware.is_linux_direct(),
1043            using_vtl0_pipette: self.using_vtl0_pipette(),
1044            using_vpci: self.boot_device_type.requires_vpci_boot(),
1045            os_flavor: self.config.firmware.os_flavor(),
1046            minimal_mode: self.minimal_mode,
1047            uses_pipette_as_init: self.uses_pipette_as_init(),
1048            enable_serial: self.enable_serial,
1049            prebuilt_initrd: self.prebuilt_initrd.clone(),
1050            has_agent_disk: self.has_agent_disk(),
1051            use_virtio_vsock: self.use_virtio_vsock,
1052            #[cfg(target_os = "linux")]
1053            vhost_vsock_guest_cid: self.vhost_vsock_guest_cid,
1054            no_vmbus: self.no_vmbus,
1055            no_hv: self.no_hv,
1056        }
1057    }
1058
1059    /// Whether pipette will run as PID 1 init in the initrd.
1060    ///
1061    /// True for non-OpenHCL Linux direct boot when a pipette binary is
1062    /// available. Pipette is injected into the initrd via CPIO and set
1063    /// as `rdinit=/pipette`.
1064    fn uses_pipette_as_init(&self) -> bool {
1065        self.config.firmware.is_linux_direct()
1066            && !self.config.firmware.is_openhcl()
1067            && self.pipette_binary.is_some()
1068    }
1069
1070    /// Whether this VM is using pipette in VTL0
1071    pub fn using_vtl0_pipette(&self) -> bool {
1072        self.uses_pipette_as_init()
1073            || self
1074                .agent_image
1075                .as_ref()
1076                .is_some_and(|x| x.contains_pipette())
1077    }
1078
1079    /// Build and run the VM, then wait for the VM to emit the expected boot
1080    /// event (if configured). Does not configure and start pipette. Should
1081    /// only be used for testing platforms that pipette does not support.
1082    pub async fn run_without_agent(self) -> anyhow::Result<PetriVm<T>> {
1083        self.run_core().await
1084    }
1085
1086    /// Build and run the VM, then wait for the VM to emit the expected boot
1087    /// event (if configured). Launches pipette and returns a client to it.
1088    pub async fn run(self) -> anyhow::Result<(PetriVm<T>, PipetteClient)> {
1089        assert!(self.using_vtl0_pipette());
1090
1091        let mut vm = self.run_core().await?;
1092        let client = vm.wait_for_agent().await?;
1093        Ok((vm, client))
1094    }
1095
1096    async fn run_core(mut self) -> anyhow::Result<PetriVm<T>> {
1097        // Add the boot disk now to allow the test to modify the boot type
1098        // Add the agent disks now to allow the test to add custom files
1099        self = self.add_boot_disk().add_agent_disks();
1100
1101        // Auto-prepare the initrd with pipette injected if needed.
1102        // This centralizes the injection logic so backends only ever
1103        // receive a prebuilt_initrd path.
1104        let _prepared_initrd_guard =
1105            if self.uses_pipette_as_init() && self.prebuilt_initrd.is_none() {
1106                let tmp = self.prepare_initrd()?;
1107                self.prebuilt_initrd = Some(tmp.to_path_buf());
1108                Some(tmp)
1109            } else {
1110                None
1111            };
1112
1113        tracing::debug!(builder = ?self);
1114
1115        let arch = self.config.arch;
1116        let expect_reset = self.expect_reset();
1117        let properties = self.properties();
1118
1119        let (mut runtime, config) = self
1120            .backend
1121            .run(
1122                self.config,
1123                self.modify_vmm_config,
1124                &self.resources,
1125                properties,
1126            )
1127            .await?;
1128        let openhcl_diag_handler = runtime.openhcl_diag();
1129        let watchdog_tasks =
1130            Self::start_watchdog_tasks(&self.resources, &mut runtime, self.enable_screenshots)?;
1131
1132        let mut vm = PetriVm {
1133            resources: self.resources,
1134            runtime,
1135            watchdog_tasks,
1136            openhcl_diag_handler,
1137
1138            arch,
1139            guest_quirks: self.guest_quirks,
1140            vmm_quirks: self.vmm_quirks,
1141            expected_boot_event: self.expected_boot_event,
1142
1143            config,
1144        };
1145
1146        if expect_reset {
1147            vm.wait_for_reset_core().await?;
1148        }
1149
1150        vm.wait_for_expected_boot_event().await?;
1151
1152        Ok(vm)
1153    }
1154
1155    fn expect_reset(&self) -> bool {
1156        self.override_expect_reset
1157            || matches!(
1158                (
1159                    self.guest_quirks.initial_reboot,
1160                    self.expected_boot_event,
1161                    &self.config.firmware,
1162                    &self.config.tpm,
1163                ),
1164                (
1165                    Some(InitialRebootCondition::Always),
1166                    Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
1167                    _,
1168                    _,
1169                ) | (
1170                    Some(InitialRebootCondition::WithTpm),
1171                    Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
1172                    _,
1173                    Some(_),
1174                )
1175            )
1176    }
1177
1178    fn start_watchdog_tasks(
1179        resources: &PetriVmResources,
1180        runtime: &mut T::VmRuntime,
1181        enable_screenshots: bool,
1182    ) -> anyhow::Result<Vec<Task<()>>> {
1183        let mut tasks = Vec::new();
1184
1185        {
1186            const TIMEOUT_DURATION_MINUTES: u64 = 10;
1187            const TIMER_DURATION: Duration = Duration::from_secs(TIMEOUT_DURATION_MINUTES * 60);
1188            let log_source = resources.log_source.clone();
1189            let inspect_task =
1190                |name,
1191                 driver: &DefaultDriver,
1192                 inspect: std::pin::Pin<Box<dyn Future<Output = _> + Send>>| {
1193                    driver.spawn(format!("petri-watchdog-inspect-{name}"), async move {
1194                        if CancelContext::new()
1195                            .with_timeout(Duration::from_secs(10))
1196                            .until_cancelled(save_inspect(name, inspect, &log_source))
1197                            .await
1198                            .is_err()
1199                        {
1200                            tracing::warn!(name, "Failed to collect inspect data within timeout");
1201                        }
1202                    })
1203                };
1204
1205            let driver = resources.driver.clone();
1206            let vmm_inspector = runtime.inspector();
1207            let openhcl_diag_handler = runtime.openhcl_diag();
1208            tasks.push(resources.driver.spawn("timer-watchdog", async move {
1209                PolledTimer::new(&driver).sleep(TIMER_DURATION).await;
1210                tracing::warn!("Test timeout reached after {TIMEOUT_DURATION_MINUTES} minutes, collecting diagnostics.");
1211                let mut timeout_tasks = Vec::new();
1212                if let Some(inspector) = vmm_inspector {
1213                    timeout_tasks.push(inspect_task.clone()("vmm", &driver, Box::pin(async move { inspector.inspect("").await })) );
1214                }
1215                if let Some(openhcl_diag_handler) = openhcl_diag_handler {
1216                    timeout_tasks.push(inspect_task("openhcl", &driver, Box::pin(async move { openhcl_diag_handler.inspect("", None, None).await })));
1217                }
1218                futures::future::join_all(timeout_tasks).await;
1219                tracing::error!("Test time out diagnostics collection complete, aborting.");
1220                panic!("Test timed out");
1221            }));
1222        }
1223
1224        if enable_screenshots {
1225            if let Some(mut framebuffer_access) = runtime.take_framebuffer_access() {
1226                let mut timer = PolledTimer::new(&resources.driver);
1227                let log_source = resources.log_source.clone();
1228
1229                tasks.push(
1230                    resources
1231                        .driver
1232                        .spawn("petri-watchdog-screenshot", async move {
1233                            let mut image = Vec::new();
1234                            let mut last_image = Vec::new();
1235                            loop {
1236                                timer.sleep(Duration::from_secs(2)).await;
1237                                tracing::trace!("Taking screenshot.");
1238
1239                                let VmScreenshotMeta {
1240                                    color,
1241                                    width,
1242                                    height,
1243                                } = match framebuffer_access.screenshot(&mut image).await {
1244                                    Ok(Some(meta)) => meta,
1245                                    Ok(None) => {
1246                                        tracing::debug!("VM off, skipping screenshot.");
1247                                        continue;
1248                                    }
1249                                    Err(e) => {
1250                                        tracing::error!(?e, "Failed to take screenshot");
1251                                        continue;
1252                                    }
1253                                };
1254
1255                                if image == last_image {
1256                                    tracing::debug!(
1257                                        "No change in framebuffer, skipping screenshot."
1258                                    );
1259                                    continue;
1260                                }
1261
1262                                let r = log_source.create_attachment("screenshot.png").and_then(
1263                                    |mut f| {
1264                                        image::write_buffer_with_format(
1265                                            &mut f,
1266                                            &image,
1267                                            width.into(),
1268                                            height.into(),
1269                                            color,
1270                                            image::ImageFormat::Png,
1271                                        )
1272                                        .map_err(Into::into)
1273                                    },
1274                                );
1275
1276                                if let Err(e) = r {
1277                                    tracing::error!(?e, "Failed to save screenshot");
1278                                } else {
1279                                    tracing::info!("Screenshot saved.");
1280                                }
1281
1282                                std::mem::swap(&mut image, &mut last_image);
1283                            }
1284                        }),
1285                );
1286            }
1287        }
1288
1289        Ok(tasks)
1290    }
1291
1292    /// Configure the test to expect a boot failure from the VM.
1293    /// Useful for negative tests.
1294    pub fn with_expect_boot_failure(mut self) -> Self {
1295        self.expected_boot_event = Some(FirmwareEvent::BootFailed);
1296        self
1297    }
1298
1299    /// Configure the test to not expect any boot event.
1300    /// Useful for tests that do not boot a VTL0 guest.
1301    pub fn with_expect_no_boot_event(mut self) -> Self {
1302        self.expected_boot_event = None;
1303        self
1304    }
1305
1306    /// Allow the VM to reset once at the beginning of the test. Should only be
1307    /// used if you are using a special VM configuration that causes the guest
1308    /// to reboot when it usually wouldn't.
1309    pub fn with_expect_reset(mut self) -> Self {
1310        self.override_expect_reset = true;
1311        self
1312    }
1313
1314    /// Set the VM to enable secure boot and inject the templates per OS flavor.
1315    pub fn with_secure_boot(mut self) -> Self {
1316        self.config
1317            .firmware
1318            .uefi_config_mut()
1319            .expect("Secure boot is only supported for UEFI firmware.")
1320            .secure_boot_enabled = true;
1321
1322        match self.os_flavor() {
1323            OsFlavor::Windows => self.with_windows_secure_boot_template(),
1324            OsFlavor::Linux => self.with_uefi_ca_secure_boot_template(),
1325            _ => panic!(
1326                "Secure boot unsupported for OS flavor {:?}",
1327                self.os_flavor()
1328            ),
1329        }
1330    }
1331
1332    /// Inject Windows secure boot templates into the VM's UEFI.
1333    pub fn with_windows_secure_boot_template(mut self) -> Self {
1334        self.config
1335            .firmware
1336            .uefi_config_mut()
1337            .expect("Secure boot is only supported for UEFI firmware.")
1338            .secure_boot_template = Some(SecureBootTemplate::MicrosoftWindows);
1339        self
1340    }
1341
1342    /// Inject UEFI CA secure boot templates into the VM's UEFI.
1343    pub fn with_uefi_ca_secure_boot_template(mut self) -> Self {
1344        self.config
1345            .firmware
1346            .uefi_config_mut()
1347            .expect("Secure boot is only supported for UEFI firmware.")
1348            .secure_boot_template = Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority);
1349        self
1350    }
1351
1352    /// Apply a custom UEFI variable delta encoded as JSON.
1353    pub fn with_custom_uefi_json(mut self, json: impl Into<Vec<u8>>) -> Self {
1354        self.config
1355            .firmware
1356            .uefi_config_mut()
1357            .expect("Custom UEFI variables are only supported for UEFI firmware.")
1358            .custom_uefi_json = Some(json.into());
1359        self
1360    }
1361
1362    /// Set the VM to use the specified processor topology.
1363    pub fn with_processor_topology(mut self, topology: ProcessorTopology) -> Self {
1364        self.config.proc_topology = topology;
1365        self
1366    }
1367
1368    /// Set the VM to use the specified memory config.
1369    pub fn with_memory(mut self, memory: MemoryConfig) -> Self {
1370        self.config.memory = memory;
1371        self
1372    }
1373
1374    /// Sets a custom OpenHCL IGVM VTL2 address type. This controls the behavior
1375    /// of where VTL2 is placed in address space, and also the total size of memory
1376    /// allocated for VTL2. VTL2 start will fail if `address_type` is specified
1377    /// and leads to the loader allocating less memory than what is in the IGVM file.
1378    pub fn with_vtl2_base_address_type(mut self, address_type: Vtl2BaseAddressType) -> Self {
1379        self.config
1380            .firmware
1381            .openhcl_config_mut()
1382            .expect("OpenHCL firmware is required to set custom VTL2 address type.")
1383            .vtl2_base_address_type = Some(address_type);
1384        self
1385    }
1386
1387    /// Sets a custom OpenHCL IGVM file to use.
1388    pub fn with_custom_openhcl(mut self, artifact: ResolvedArtifact<impl IsOpenhclIgvm>) -> Self {
1389        match &mut self.config.firmware {
1390            Firmware::OpenhclLinuxDirect { igvm_path, .. }
1391            | Firmware::OpenhclPcat { igvm_path, .. }
1392            | Firmware::OpenhclUefi { igvm_path, .. } => {
1393                *igvm_path = artifact.erase();
1394            }
1395            Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } | Firmware::Pcat { .. } => {
1396                panic!("Custom OpenHCL is only supported for OpenHCL firmware.")
1397            }
1398        }
1399        self
1400    }
1401
1402    /// Append additional command line arguments to pass to the paravisor.
1403    pub fn with_openhcl_command_line(mut self, additional_command_line: &str) -> Self {
1404        append_cmdline(
1405            &mut self
1406                .config
1407                .firmware
1408                .openhcl_config_mut()
1409                .expect("OpenHCL command line is only supported for OpenHCL firmware.")
1410                .custom_command_line,
1411            additional_command_line,
1412        );
1413        self
1414    }
1415
1416    /// Configure whether OpenHCL enables MANA keepalive at boot.
1417    pub fn with_mana_keepalive(mut self, enable: bool) -> Self {
1418        self.config
1419            .firmware
1420            .openhcl_config_mut()
1421            .expect("MANA keepalive is only supported for OpenHCL firmware.")
1422            .enable_mana_keepalive = enable;
1423        self
1424    }
1425
1426    /// Enable confidential filtering, even if the VM is not confidential.
1427    pub fn with_confidential_filtering(self) -> Self {
1428        if !self.config.firmware.is_openhcl() {
1429            panic!("Confidential filtering is only supported for OpenHCL");
1430        }
1431        self.with_openhcl_command_line(&format!(
1432            "{}=1 {}=0",
1433            underhill_confidentiality::OPENHCL_CONFIDENTIAL_ENV_VAR_NAME,
1434            underhill_confidentiality::OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME
1435        ))
1436    }
1437
1438    /// Sets the command line parameters passed to OpenHCL related to logging.
1439    pub fn with_openhcl_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1440        self.config
1441            .firmware
1442            .openhcl_config_mut()
1443            .expect("OpenHCL firmware is required to set custom OpenHCL log levels.")
1444            .log_levels = levels;
1445        self
1446    }
1447
1448    /// Sets the log levels for the host OpenVMM process.
1449    /// DEVNOTE: In the future, this could be generalized for both HyperV and OpenVMM.
1450    /// For now, this is only implemented for OpenVMM.
1451    pub fn with_host_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1452        if let OpenvmmLogConfig::Custom(ref custom_levels) = levels {
1453            for key in custom_levels.keys() {
1454                if !["OPENVMM_LOG", "OPENVMM_SHOW_SPANS"].contains(&key.as_str()) {
1455                    panic!("Unsupported OpenVMM log level key: {}", key);
1456                }
1457            }
1458        }
1459
1460        self.config.host_log_levels = Some(levels.clone());
1461        self
1462    }
1463
1464    /// Adds a file to the VM's pipette agent image.
1465    pub fn with_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
1466        self.agent_image
1467            .as_mut()
1468            .expect("no guest pipette")
1469            .add_file(name, artifact);
1470        self
1471    }
1472
1473    /// Adds a file to the paravisor's pipette agent image.
1474    pub fn with_openhcl_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
1475        self.openhcl_agent_image
1476            .as_mut()
1477            .expect("no openhcl pipette")
1478            .add_file(name, artifact);
1479        self
1480    }
1481
1482    /// Sets whether UEFI frontpage is enabled.
1483    pub fn with_uefi_frontpage(mut self, enable: bool) -> Self {
1484        self.config
1485            .firmware
1486            .uefi_config_mut()
1487            .expect("UEFI frontpage is only supported for UEFI firmware.")
1488            .disable_frontpage = !enable;
1489        self
1490    }
1491
1492    /// Sets the UEFI diagnostics log level filter.
1493    ///
1494    /// By default only ERROR and WARN level entries are forwarded to the
1495    /// host tracing infrastructure. Use this to also surface INFO (or all)
1496    /// entries when a test needs to observe them.
1497    pub fn with_efi_diagnostics_log_level(mut self, level: EfiDiagnosticsLogLevel) -> Self {
1498        self.config
1499            .firmware
1500            .uefi_config_mut()
1501            .expect("EFI diagnostics log level is only supported for UEFI firmware.")
1502            .efi_diagnostics_log_level = level;
1503        self
1504    }
1505
1506    /// Sets the per-period rate-limit override for UEFI diagnostics emission.
1507    ///
1508    /// - Not called: use the built-in defaults.
1509    /// - `0`: disable rate limiting entirely (emit every entry).
1510    /// - `n > 0`: use `n` as the per-period limit.
1511    pub fn with_efi_diagnostics_rate_limit(mut self, limit: u32) -> Self {
1512        self.config
1513            .firmware
1514            .uefi_config_mut()
1515            .expect("EFI diagnostics rate limit is only supported for UEFI firmware.")
1516            .efi_diagnostics_rate_limit = Some(limit);
1517        self
1518    }
1519
1520    /// Sets whether UEFI should always attempt a default boot.
1521    pub fn with_default_boot_always_attempt(mut self, enable: bool) -> Self {
1522        self.config
1523            .firmware
1524            .uefi_config_mut()
1525            .expect("Default boot always attempt is only supported for UEFI firmware.")
1526            .default_boot_always_attempt = enable;
1527        self
1528    }
1529
1530    /// Force UEFI to bounce-buffer all DMA traffic.
1531    pub fn with_uefi_force_dma_bounce(mut self, enable: bool) -> Self {
1532        self.config
1533            .firmware
1534            .uefi_config_mut()
1535            .expect("force DMA bounce is only supported for UEFI firmware.")
1536            .force_dma_bounce = enable;
1537        self
1538    }
1539
1540    /// Run the VM with Enable VMBus relay enabled
1541    pub fn with_vmbus_redirect(mut self, enable: bool) -> Self {
1542        self.config
1543            .firmware
1544            .openhcl_config_mut()
1545            .expect("VMBus redirection is only supported for OpenHCL firmware.")
1546            .vmbus_redirect = enable;
1547        self
1548    }
1549
1550    /// Enable guest hibernation support.
1551    ///
1552    /// Applies to any firmware type: for OpenHCL this sets the DPS
1553    /// `enable_hibernation` flag; for OpenVMM UEFI/PCAT firmware it enables the
1554    /// firmware's hibernation support.
1555    pub fn with_hibernation_enabled(mut self, enable: bool) -> Self {
1556        self.config.hibernation_enabled = enable;
1557        self
1558    }
1559
1560    /// Enable the IPMI KCS interface for an OpenHCL UEFI VM.
1561    pub fn with_ipmi(mut self, enable: bool) -> Self {
1562        self.config.ipmi_enabled = enable;
1563        self
1564    }
1565
1566    /// Specify the guest state lifetime for the VM
1567    pub fn with_guest_state_lifetime(
1568        mut self,
1569        guest_state_lifetime: PetriGuestStateLifetime,
1570    ) -> Self {
1571        let disk = match self.config.vmgs {
1572            PetriVmgsResource::Disk(disk)
1573            | PetriVmgsResource::ReprovisionOnFailure(disk)
1574            | PetriVmgsResource::Reprovision(disk) => disk,
1575            PetriVmgsResource::Ephemeral => PetriVmgsDisk::default(),
1576        };
1577        self.config.vmgs = match guest_state_lifetime {
1578            PetriGuestStateLifetime::Disk => PetriVmgsResource::Disk(disk),
1579            PetriGuestStateLifetime::ReprovisionOnFailure => {
1580                PetriVmgsResource::ReprovisionOnFailure(disk)
1581            }
1582            PetriGuestStateLifetime::Reprovision => PetriVmgsResource::Reprovision(disk),
1583            PetriGuestStateLifetime::Ephemeral => {
1584                if !matches!(disk.disk, Disk::Memory(_)) {
1585                    panic!("attempted to use ephemeral guest state after specifying backing vmgs")
1586                }
1587                PetriVmgsResource::Ephemeral
1588            }
1589        };
1590        self
1591    }
1592
1593    /// Specify the guest state encryption policy for the VM
1594    pub fn with_guest_state_encryption(mut self, policy: GuestStateEncryptionPolicy) -> Self {
1595        match &mut self.config.vmgs {
1596            PetriVmgsResource::Disk(vmgs)
1597            | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1598            | PetriVmgsResource::Reprovision(vmgs) => {
1599                vmgs.encryption_policy = policy;
1600            }
1601            PetriVmgsResource::Ephemeral => {
1602                panic!("attempted to encrypt ephemeral guest state")
1603            }
1604        }
1605        self
1606    }
1607
1608    /// Use the specified backing VMGS file
1609    pub fn with_initial_vmgs(self, disk: ResolvedArtifact<impl IsTestVmgs>) -> Self {
1610        self.with_backing_vmgs(Disk::Differencing(DiskPath::Local(disk.into())))
1611    }
1612
1613    /// Use the specified backing VMGS file
1614    pub fn with_persistent_vmgs(self, disk: impl AsRef<Path>) -> Self {
1615        self.with_backing_vmgs(Disk::Persistent(disk.as_ref().to_path_buf()))
1616    }
1617
1618    fn with_backing_vmgs(mut self, disk: Disk) -> Self {
1619        match &mut self.config.vmgs {
1620            PetriVmgsResource::Disk(vmgs)
1621            | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1622            | PetriVmgsResource::Reprovision(vmgs) => {
1623                if !matches!(vmgs.disk, Disk::Memory(_)) {
1624                    panic!("already specified a backing vmgs file");
1625                }
1626                vmgs.disk = disk;
1627            }
1628            PetriVmgsResource::Ephemeral => {
1629                panic!("attempted to specify a backing vmgs with ephemeral guest state")
1630            }
1631        }
1632        self
1633    }
1634
1635    /// Set the boot device type for the VM.
1636    ///
1637    /// This overrides the default, which is determined by the firmware type.
1638    pub fn with_boot_device_type(mut self, boot: BootDeviceType) -> Self {
1639        self.boot_device_type = boot;
1640        self
1641    }
1642
1643    /// Override the PCIe root port that the boot NVMe controller is placed on
1644    /// when using [`BootDeviceType::PcieNvme`].
1645    ///
1646    /// The named port must exist in the PCIe topology added via the backend's
1647    /// `with_pcie_root_topology`. Defaults to `s0rc0rp0`.
1648    pub fn with_pcie_boot_port(mut self, port_name: &str) -> Self {
1649        self.pcie_boot_port = Some(port_name.to_string());
1650        self
1651    }
1652
1653    /// Enable the TPM for the VM.
1654    pub fn with_tpm(mut self, enable: bool) -> Self {
1655        if enable {
1656            self.config.tpm.get_or_insert_default();
1657        } else {
1658            self.config.tpm = None;
1659        }
1660        self
1661    }
1662
1663    /// Enable or disable the TPM state persistence for the VM.
1664    pub fn with_tpm_state_persistence(mut self, tpm_state_persistence: bool) -> Self {
1665        self.config
1666            .tpm
1667            .as_mut()
1668            .expect("TPM persistence requires a TPM")
1669            .no_persistent_secrets = !tpm_state_persistence;
1670        self
1671    }
1672
1673    /// Set the hardware sealing policy for the VM's TPM.
1674    pub fn with_hardware_sealing_policy(mut self, policy: PetriHardwareSealingPolicy) -> Self {
1675        self.config
1676            .tpm
1677            .as_mut()
1678            .expect("hardware sealing policy requires a TPM")
1679            .hardware_sealing_policy = policy;
1680        self
1681    }
1682
1683    /// Select which TPM reference implementation version the VM's TPM runs.
1684    pub fn with_tpm_version(mut self, version: PetriTpmVersion) -> Self {
1685        self.config
1686            .tpm
1687            .as_mut()
1688            .expect("TPM version requires a TPM")
1689            .version = version;
1690        self
1691    }
1692
1693    /// Add custom VTL 2 settings.
1694    // TODO: At some point we want to replace uses of this with nicer with_disk,
1695    // with_nic, etc. methods.
1696    pub fn with_custom_vtl2_settings(
1697        mut self,
1698        f: impl FnOnce(&mut Vtl2Settings) + 'static + Send + Sync,
1699    ) -> Self {
1700        f(self
1701            .config
1702            .firmware
1703            .vtl2_settings()
1704            .expect("Custom VTL 2 settings are only supported with OpenHCL"));
1705        self
1706    }
1707
1708    /// Add a storage controller to VTL2
1709    pub fn add_vtl2_storage_controller(self, controller: StorageController) -> Self {
1710        self.with_custom_vtl2_settings(move |v| {
1711            v.dynamic
1712                .as_mut()
1713                .unwrap()
1714                .storage_controllers
1715                .push(controller)
1716        })
1717    }
1718
1719    /// Add an additional SCSI controller to the VM.
1720    pub fn add_vmbus_storage_controller(
1721        mut self,
1722        id: &Guid,
1723        target_vtl: Vtl,
1724        controller_type: VmbusStorageType,
1725    ) -> Self {
1726        if self
1727            .config
1728            .vmbus_storage_controllers
1729            .insert(
1730                *id,
1731                VmbusStorageController::new(target_vtl, controller_type),
1732            )
1733            .is_some()
1734        {
1735            panic!("storage controller {id} already existed");
1736        }
1737        self
1738    }
1739
1740    /// Add a VMBus disk drive to the VM
1741    pub fn add_vmbus_drive(
1742        mut self,
1743        drive: Drive,
1744        controller_id: &Guid,
1745        controller_location: Option<u32>,
1746    ) -> Self {
1747        let controller = self
1748            .config
1749            .vmbus_storage_controllers
1750            .get_mut(controller_id)
1751            .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
1752
1753        _ = controller.set_drive(controller_location, drive, false);
1754
1755        self
1756    }
1757
1758    /// Add a VMBus disk drive to the VM
1759    pub fn add_ide_drive(
1760        mut self,
1761        drive: Drive,
1762        controller_number: u32,
1763        controller_location: u8,
1764    ) -> Self {
1765        self.config
1766            .firmware
1767            .ide_controllers_mut()
1768            .expect("Host IDE requires PCAT with no HCL")[controller_number as usize]
1769            [controller_location as usize] = Some(drive);
1770
1771        self
1772    }
1773
1774    /// Add a physical NVMe device to the VM
1775    pub fn add_physical_nvme_device(mut self, vsid: Guid, device: PhysicalNvmeDevice) -> Self {
1776        if self
1777            .config
1778            .physical_nvme_devices
1779            .insert(vsid, device)
1780            .is_some()
1781        {
1782            panic!("physical NVMe device {vsid} already existed");
1783        }
1784        self
1785    }
1786
1787    /// Get VM's guest OS flavor
1788    pub fn os_flavor(&self) -> OsFlavor {
1789        self.config.firmware.os_flavor()
1790    }
1791
1792    /// Get whether the VM will use OpenHCL
1793    pub fn is_openhcl(&self) -> bool {
1794        self.config.firmware.is_openhcl()
1795    }
1796
1797    /// Get the isolation type of the VM
1798    pub fn isolation(&self) -> Option<IsolationType> {
1799        self.config.firmware.isolation()
1800    }
1801
1802    /// Get the machine architecture
1803    pub fn arch(&self) -> MachineArch {
1804        self.config.arch
1805    }
1806
1807    /// Get the log source for creating additional log files.
1808    pub fn log_source(&self) -> &PetriLogSource {
1809        &self.resources.log_source
1810    }
1811
1812    /// Get the default OpenHCL servicing flags for this config
1813    pub fn default_servicing_flags(&self) -> OpenHclServicingFlags {
1814        T::default_servicing_flags()
1815    }
1816
1817    /// Get the backend-specific config builder
1818    pub fn modify_backend(
1819        mut self,
1820        f: impl FnOnce(T::VmmConfig) -> T::VmmConfig + 'static + Send,
1821    ) -> Self {
1822        if self.modify_vmm_config.is_some() {
1823            panic!("only one modify_backend allowed");
1824        }
1825        self.modify_vmm_config = Some(ModifyFn(Box::new(f)));
1826        self
1827    }
1828}
1829
1830impl<T: PetriVmmBackend> PetriVm<T> {
1831    /// Immediately tear down the VM.
1832    pub async fn teardown(self) -> anyhow::Result<()> {
1833        tracing::info!("Tearing down VM...");
1834        self.runtime.teardown().await
1835    }
1836
1837    /// Wait for the VM to halt, returning the reason for the halt.
1838    pub async fn wait_for_halt(&mut self) -> anyhow::Result<PetriHaltReasonDetail> {
1839        tracing::info!("Waiting for VM to halt...");
1840        let halt_reason = self.runtime.wait_for_halt(false).await?;
1841        tracing::info!("VM halted: {halt_reason:?}. Cancelling watchdogs...");
1842        futures::future::join_all(self.watchdog_tasks.drain(..).map(|t| t.cancel())).await;
1843        Ok(halt_reason)
1844    }
1845
1846    /// Wait for the VM to cleanly shutdown.
1847    pub async fn wait_for_clean_shutdown(&mut self) -> anyhow::Result<()> {
1848        let halt_reason = self.wait_for_halt().await?;
1849        if halt_reason.reason != PetriHaltReason::PowerOff {
1850            anyhow::bail!("Expected PowerOff, got {halt_reason:?}");
1851        }
1852        tracing::info!("VM was cleanly powered off and torn down.");
1853        Ok(())
1854    }
1855
1856    /// Wait for the VM to halt, returning the reason for the halt,
1857    /// and tear down the VM.
1858    pub async fn wait_for_teardown(mut self) -> anyhow::Result<PetriHaltReasonDetail> {
1859        let halt_reason = self.wait_for_halt().await?;
1860        self.teardown().await?;
1861        Ok(halt_reason)
1862    }
1863
1864    /// Wait for the VM to cleanly shutdown and tear down the VM.
1865    pub async fn wait_for_clean_teardown(mut self) -> anyhow::Result<()> {
1866        self.wait_for_clean_shutdown().await?;
1867        self.teardown().await
1868    }
1869
1870    /// Wait for the VM to reset. Does not wait for pipette.
1871    pub async fn wait_for_reset_no_agent(&mut self) -> anyhow::Result<()> {
1872        self.wait_for_reset_core().await?;
1873        self.wait_for_expected_boot_event().await?;
1874        Ok(())
1875    }
1876
1877    /// Wait for the VM to reset and pipette to connect.
1878    pub async fn wait_for_reset(&mut self) -> anyhow::Result<PipetteClient> {
1879        self.wait_for_reset_no_agent().await?;
1880        self.wait_for_agent().await
1881    }
1882
1883    async fn wait_for_reset_core(&mut self) -> anyhow::Result<()> {
1884        tracing::info!("Waiting for VM to reset...");
1885        let halt_reason = self.runtime.wait_for_halt(true).await?;
1886        if halt_reason.reason != PetriHaltReason::Reset {
1887            anyhow::bail!("Expected reset, got {halt_reason:?}");
1888        }
1889        tracing::info!("VM reset.");
1890        Ok(())
1891    }
1892
1893    /// Invoke Inspect on the running OpenHCL instance.
1894    ///
1895    /// IMPORTANT: As mentioned in the Guide, inspect output is *not* guaranteed
1896    /// to be stable. Use this to test that components in OpenHCL are working as
1897    /// you would expect. But, if you are adding a test simply to verify that
1898    /// the inspect output as some other tool depends on it, then that is
1899    /// incorrect.
1900    ///
1901    /// - `timeout` is enforced on the client side
1902    /// - `path` and `depth` are passed to the [`inspect::Inspect`] machinery.
1903    pub async fn inspect_openhcl(
1904        &self,
1905        path: impl Into<String>,
1906        depth: Option<usize>,
1907        timeout: Option<Duration>,
1908    ) -> anyhow::Result<inspect::Node> {
1909        self.openhcl_diag()?
1910            .inspect(path.into().as_str(), depth, timeout)
1911            .await
1912    }
1913
1914    /// Invoke Update (Inspect protocol) on the running OpenHCL instance.
1915    ///
1916    /// IMPORTANT: As mentioned in the Guide, inspect output is *not* guaranteed
1917    /// to be stable. Use this to test that components in OpenHCL are working as
1918    /// you would expect. But, if you are adding a test simply to verify that
1919    /// the inspect output as some other tool depends on it, then that is
1920    /// incorrect.
1921    ///
1922    /// - `path` and `value` are passed to the [`inspect::Inspect`] machinery.
1923    pub async fn inspect_update_openhcl(
1924        &self,
1925        path: impl Into<String>,
1926        value: impl Into<String>,
1927    ) -> anyhow::Result<inspect::Value> {
1928        self.openhcl_diag()?
1929            .inspect_update(path.into(), value.into())
1930            .await
1931    }
1932
1933    /// Test that we are able to inspect OpenHCL.
1934    pub async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()> {
1935        self.inspect_openhcl("", None, None).await.map(|_| ())
1936    }
1937
1938    /// Invoke Inspect on the running VMM process itself (e.g. OpenVMM),
1939    /// returning the inspect tree rooted at `path` (pass `""` for the whole
1940    /// tree).
1941    ///
1942    /// Only backends that expose an inspect interface (currently OpenVMM)
1943    /// support this; other backends return an error.
1944    ///
1945    /// IMPORTANT: As mentioned in the Guide, inspect output is *not* guaranteed
1946    /// to be stable. Use this to verify that components are working as you
1947    /// expect, not to assert on output that some other tool depends on.
1948    pub async fn inspect_vmm(&self, path: &str) -> anyhow::Result<inspect::Node> {
1949        use anyhow::Context;
1950
1951        let inspector = self
1952            .runtime
1953            .inspector()
1954            .context("this VMM backend does not support inspect")?;
1955        inspector.inspect(path).await
1956    }
1957
1958    /// Wait for VTL 2 to report that it is ready to respond to commands.
1959    /// Will fail if the VM is not running OpenHCL.
1960    ///
1961    /// This should only be necessary if you're doing something manual. All
1962    /// Petri-provided methods will wait for VTL 2 to be ready automatically.
1963    pub async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()> {
1964        self.openhcl_diag()?.wait_for_vtl2().await
1965    }
1966
1967    /// Get the kmsg stream from OpenHCL.
1968    pub async fn kmsg(&self) -> anyhow::Result<diag_client::kmsg_stream::KmsgStream> {
1969        self.openhcl_diag()?.kmsg().await
1970    }
1971
1972    /// Gets a live core dump of the OpenHCL process specified by 'name' and
1973    /// writes it to 'path'
1974    pub async fn openhcl_core_dump(&self, name: &str, path: &Path) -> anyhow::Result<()> {
1975        self.openhcl_diag()?.core_dump(name, path).await
1976    }
1977
1978    /// Crashes the specified openhcl process
1979    pub async fn openhcl_crash(&self, name: &str) -> anyhow::Result<()> {
1980        self.openhcl_diag()?.crash(name).await
1981    }
1982
1983    /// Wait for a connection from a pipette agent running in the guest.
1984    /// Useful if you've rebooted the vm or are otherwise expecting a fresh connection.
1985    async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient> {
1986        // As a workaround for #2470 (where the guest crashes when the pipette
1987        // connection timeout expires due to a vmbus bug), wait for the shutdown
1988        // IC to come online first so that we probably won't time out when
1989        // connecting to the agent.
1990        // TODO: remove this once the bug is fixed, since it shouldn't be
1991        // necessary and a guest could in theory support pipette and not the IC
1992        //
1993        // This is a no-op when the shutdown IC is not configured (e.g.,
1994        // no VMBus or minimal mode).
1995        self.runtime.wait_for_enlightened_shutdown_ready().await?;
1996        self.runtime.wait_for_agent(false).await
1997    }
1998
1999    /// Wait for a connection from a pipette agent running in VTL 2.
2000    /// Useful if you've reset VTL 2 or are otherwise expecting a fresh connection.
2001    /// Will fail if the VM is not running OpenHCL.
2002    pub async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient> {
2003        // VTL 2's pipette doesn't auto launch, only launch it on demand
2004        self.launch_vtl2_pipette().await?;
2005        self.runtime.wait_for_agent(true).await
2006    }
2007
2008    /// Waits for an event emitted by the firmware about its boot status, and
2009    /// verifies that it is the expected success value.
2010    ///
2011    /// * Linux Direct guests do not emit a boot event, so this method immediately returns Ok.
2012    /// * PCAT guests may not emit an event depending on the PCAT version, this
2013    ///   method is best effort for them.
2014    async fn wait_for_expected_boot_event(&mut self) -> anyhow::Result<()> {
2015        if let Some(expected_event) = self.expected_boot_event {
2016            let event = self.wait_for_boot_event().await?;
2017
2018            anyhow::ensure!(
2019                event == expected_event,
2020                "Did not receive expected boot event"
2021            );
2022        } else {
2023            tracing::warn!("Boot event not emitted for configured firmware or manually ignored.");
2024        }
2025
2026        Ok(())
2027    }
2028
2029    /// Waits for an event emitted by the firmware about its boot status, and
2030    /// returns that status.
2031    async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
2032        tracing::info!("Waiting for boot event...");
2033        let boot_event = loop {
2034            if let Some(event) = self
2035                .runtime
2036                .wait_for_boot_event(self.vmm_quirks.flaky_boot)
2037                .await?
2038            {
2039                break event;
2040            }
2041
2042            tracing::error!("Did not get boot event in required time, resetting...");
2043            if let Some(inspector) = self.runtime.inspector() {
2044                save_inspect(
2045                    "vmm",
2046                    Box::pin(async move { inspector.inspect("").await }),
2047                    &self.resources.log_source,
2048                )
2049                .await;
2050            }
2051
2052            self.runtime.reset().await?;
2053        };
2054        tracing::info!("Got boot event: {boot_event:?}");
2055        Ok(boot_event)
2056    }
2057
2058    /// Wait for the Hyper-V shutdown IC to be ready and use it to instruct
2059    /// the guest to shutdown.
2060    pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
2061        tracing::info!("Waiting for enlightened shutdown to be ready");
2062        self.runtime.wait_for_enlightened_shutdown_ready().await?;
2063
2064        // all guests used in testing have been observed to intermittently
2065        // drop shutdown requests if they are sent too soon after the shutdown
2066        // ic comes online. give them a little extra time.
2067        // TODO: use a different method of determining whether the VM has booted
2068        // or debug and fix the shutdown IC.
2069        let mut wait_time = Duration::from_secs(10);
2070
2071        // some guests need even more time
2072        if let Some(duration) = self.guest_quirks.hyperv_shutdown_ic_sleep {
2073            wait_time += duration;
2074        }
2075
2076        tracing::info!(
2077            "Shutdown IC reported ready, waiting for an extra {}s",
2078            wait_time.as_secs()
2079        );
2080        PolledTimer::new(&self.resources.driver)
2081            .sleep(wait_time)
2082            .await;
2083
2084        tracing::info!("Sending enlightened shutdown command");
2085        self.runtime.send_enlightened_shutdown(kind).await
2086    }
2087
2088    /// Instruct the OpenHCL to restart the VTL2 paravisor. Will fail if the VM
2089    /// is not running OpenHCL. Will also fail if the VM is not running.
2090    pub async fn restart_openhcl(
2091        &mut self,
2092        new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
2093        flags: OpenHclServicingFlags,
2094    ) -> anyhow::Result<()> {
2095        self.runtime
2096            .restart_openhcl(&new_openhcl.erase(), flags)
2097            .await
2098    }
2099
2100    /// Update the command line parameter of the running VM that will apply on next boot.
2101    /// Will fail if the VM is not using IGVM load mode.
2102    pub async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()> {
2103        self.runtime.update_command_line(command_line).await
2104    }
2105
2106    /// Hot-add a PCIe device to a named port at runtime.
2107    pub async fn add_pcie_device(
2108        &mut self,
2109        port_name: String,
2110        resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
2111    ) -> anyhow::Result<()> {
2112        self.runtime.add_pcie_device(port_name, resource).await
2113    }
2114
2115    /// Hot-remove a PCIe device from a named port at runtime.
2116    pub async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
2117        self.runtime.remove_pcie_device(port_name).await
2118    }
2119
2120    /// Instruct the OpenHCL to save the state of the VTL2 paravisor. Will fail if the VM
2121    /// is not running OpenHCL. Will also fail if the VM is not running or if this is called twice in succession
2122    pub async fn save_openhcl(
2123        &mut self,
2124        new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
2125        flags: OpenHclServicingFlags,
2126    ) -> anyhow::Result<()> {
2127        self.runtime.save_openhcl(&new_openhcl.erase(), flags).await
2128    }
2129
2130    /// Instruct the OpenHCL to restore the state of the VTL2 paravisor. Will fail if the VM
2131    /// is not running OpenHCL. Will also fail if the VM is running or if this is called without prior save
2132    pub async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
2133        self.runtime.restore_openhcl().await
2134    }
2135
2136    /// Get VM's guest OS flavor
2137    pub fn arch(&self) -> MachineArch {
2138        self.arch
2139    }
2140
2141    /// Get the inner runtime backend to make backend-specific calls
2142    pub fn backend(&mut self) -> &mut T::VmRuntime {
2143        &mut self.runtime
2144    }
2145
2146    async fn launch_vtl2_pipette(&self) -> anyhow::Result<()> {
2147        tracing::debug!("Launching VTL 2 pipette...");
2148
2149        // Start pipette through DiagClient
2150        let res = self
2151            .openhcl_diag()?
2152            .run_vtl2_command("sh", &["-c", "mkdir /cidata && mount LABEL=cidata /cidata"])
2153            .await?;
2154
2155        if !res.exit_status.success() {
2156            anyhow::bail!("Failed to mount VTL 2 pipette drive: {:?}", res);
2157        }
2158
2159        let res = self
2160            .openhcl_diag()?
2161            .run_detached_vtl2_command("sh", &["-c", "/cidata/pipette 2>&1 | logger &"])
2162            .await?;
2163
2164        if !res.success() {
2165            anyhow::bail!("Failed to spawn VTL 2 pipette: {:?}", res);
2166        }
2167
2168        Ok(())
2169    }
2170
2171    fn openhcl_diag(&self) -> anyhow::Result<&OpenHclDiagHandler> {
2172        if let Some(ohd) = self.openhcl_diag_handler.as_ref() {
2173            Ok(ohd)
2174        } else {
2175            anyhow::bail!("VM is not configured with OpenHCL")
2176        }
2177    }
2178
2179    /// Get the path to the VM's guest state file
2180    pub async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2181        self.runtime.get_guest_state_file().await
2182    }
2183
2184    /// Modify OpenHCL VTL2 settings.
2185    pub async fn modify_vtl2_settings(
2186        &mut self,
2187        f: impl FnOnce(&mut Vtl2Settings),
2188    ) -> anyhow::Result<()> {
2189        if self.openhcl_diag_handler.is_none() {
2190            panic!("Custom VTL 2 settings are only supported with OpenHCL");
2191        }
2192        f(self
2193            .config
2194            .vtl2_settings
2195            .get_or_insert_with(default_vtl2_settings));
2196        self.runtime
2197            .set_vtl2_settings(self.config.vtl2_settings.as_ref().unwrap())
2198            .await
2199    }
2200
2201    /// Get the list of storage controllers added to this VM
2202    pub fn get_vmbus_storage_controllers(&self) -> &HashMap<Guid, VmbusStorageController> {
2203        &self.config.vmbus_storage_controllers
2204    }
2205
2206    /// Add or modify a VMBus disk drive
2207    pub async fn set_vmbus_drive(
2208        &mut self,
2209        drive: Drive,
2210        controller_id: &Guid,
2211        controller_location: Option<u32>,
2212    ) -> anyhow::Result<()> {
2213        let controller = self
2214            .config
2215            .vmbus_storage_controllers
2216            .get_mut(controller_id)
2217            .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
2218
2219        let controller_location = controller.set_drive(controller_location, drive, true);
2220        let disk = controller.drives.get(&controller_location).unwrap();
2221
2222        self.runtime
2223            .set_vmbus_drive(disk, controller_id, controller_location)
2224            .await?;
2225
2226        Ok(())
2227    }
2228}
2229
2230/// A running VM that tests can interact with.
2231#[async_trait]
2232pub trait PetriVmRuntime: Send + Sync + 'static {
2233    /// Interface for inspecting the VM
2234    type VmInspector: PetriVmInspector;
2235    /// Interface for accessing the framebuffer
2236    type VmFramebufferAccess: PetriVmFramebufferAccess;
2237
2238    /// Cleanly tear down the VM immediately.
2239    async fn teardown(self) -> anyhow::Result<()>;
2240    /// Wait for the VM to halt, returning the reason for the halt. The VM
2241    /// should automatically restart the VM on reset if `allow_reset` is true.
2242    async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReasonDetail>;
2243    /// Wait for a connection from a pipette agent
2244    async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>;
2245    /// Get an OpenHCL diagnostics handler for the VM
2246    fn openhcl_diag(&self) -> Option<OpenHclDiagHandler>;
2247    /// Waits for an event emitted by the firmware about its boot status, and
2248    /// returns that status. Returns `None` if `timeout` elapsed first.
2249    async fn wait_for_boot_event(
2250        &mut self,
2251        timeout: Option<Duration>,
2252    ) -> anyhow::Result<Option<FirmwareEvent>>;
2253    /// Waits for the Hyper-V shutdown IC to be ready
2254    // TODO: return a receiver that will be closed when it is no longer ready.
2255    async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()>;
2256    /// Instruct the guest to shutdown via the Hyper-V shutdown IC.
2257    async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
2258    /// Instruct the OpenHCL to restart the VTL2 paravisor. Will fail if the VM
2259    /// is not running OpenHCL. Will also fail if the VM is not running.
2260    async fn restart_openhcl(
2261        &mut self,
2262        new_openhcl: &ResolvedArtifact,
2263        flags: OpenHclServicingFlags,
2264    ) -> anyhow::Result<()>;
2265    /// Instruct the OpenHCL to save the state of the VTL2 paravisor. Will fail if the VM
2266    /// is not running OpenHCL. Will also fail if the VM is not running or if this is called twice in succession
2267    /// without a call to `restore_openhcl`.
2268    async fn save_openhcl(
2269        &mut self,
2270        new_openhcl: &ResolvedArtifact,
2271        flags: OpenHclServicingFlags,
2272    ) -> anyhow::Result<()>;
2273    /// Instruct the OpenHCL to restore the state of the VTL2 paravisor. Will fail if the VM
2274    /// is not running OpenHCL. Will also fail if the VM is running or if this is called without prior save.
2275    async fn restore_openhcl(&mut self) -> anyhow::Result<()>;
2276    /// Update the command line parameter of the running VM that will apply on next boot.
2277    /// Will fail if the VM is not using IGVM load mode.
2278    async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()>;
2279    /// If the backend supports it, get an inspect interface
2280    fn inspector(&self) -> Option<Self::VmInspector> {
2281        None
2282    }
2283    /// If the backend supports it, take the screenshot interface
2284    /// (subsequent calls may return None).
2285    fn take_framebuffer_access(&mut self) -> Option<Self::VmFramebufferAccess> {
2286        None
2287    }
2288    /// Issue a hard reset to the VM
2289    async fn reset(&mut self) -> anyhow::Result<()>;
2290    /// Get the path to the VM's guest state file
2291    async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2292        Ok(None)
2293    }
2294    /// Set the OpenHCL VTL2 settings
2295    async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()>;
2296    /// Add or modify a VMBus disk drive
2297    async fn set_vmbus_drive(
2298        &mut self,
2299        disk: &Drive,
2300        controller_id: &Guid,
2301        controller_location: u32,
2302    ) -> anyhow::Result<()>;
2303    /// Hot-add a PCIe device to a named port at runtime.
2304    async fn add_pcie_device(
2305        &mut self,
2306        port_name: String,
2307        resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
2308    ) -> anyhow::Result<()> {
2309        let _ = (port_name, resource);
2310        anyhow::bail!("PCIe hotplug not supported by this backend")
2311    }
2312    /// Hot-remove a PCIe device from a named port at runtime.
2313    async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
2314        let _ = port_name;
2315        anyhow::bail!("PCIe hotplug not supported by this backend")
2316    }
2317}
2318
2319/// Interface for getting information about the state of the VM
2320#[async_trait]
2321pub trait PetriVmInspector: Send + Sync + 'static {
2322    /// Get information about the state of the VM at the given inspect `path`.
2323    /// Pass `""` to inspect the entire tree.
2324    async fn inspect(&self, path: &str) -> anyhow::Result<inspect::Node>;
2325}
2326
2327/// Use this for the associated type if not supported
2328pub struct NoPetriVmInspector;
2329#[async_trait]
2330impl PetriVmInspector for NoPetriVmInspector {
2331    async fn inspect(&self, _path: &str) -> anyhow::Result<inspect::Node> {
2332        unreachable!()
2333    }
2334}
2335
2336/// Raw VM screenshot
2337pub struct VmScreenshotMeta {
2338    /// color encoding used by the image
2339    pub color: image::ExtendedColorType,
2340    /// x dimension
2341    pub width: u16,
2342    /// y dimension
2343    pub height: u16,
2344}
2345
2346/// Interface for getting screenshots of the VM
2347#[async_trait]
2348pub trait PetriVmFramebufferAccess: Send + 'static {
2349    /// Populates the provided buffer with a screenshot of the VM,
2350    /// returning the dimensions and color type.
2351    async fn screenshot(&mut self, image: &mut Vec<u8>)
2352    -> anyhow::Result<Option<VmScreenshotMeta>>;
2353}
2354
2355/// Common processor topology information for the VM.
2356#[derive(Debug)]
2357pub struct ProcessorTopology {
2358    /// The number of virtual processors.
2359    pub vp_count: u32,
2360    /// Whether SMT (hyperthreading) is enabled.
2361    pub enable_smt: Option<bool>,
2362    /// The number of virtual processors per socket.
2363    pub vps_per_socket: Option<u32>,
2364    /// The APIC configuration (x86-64 only).
2365    pub apic_mode: Option<ApicMode>,
2366}
2367
2368impl Default for ProcessorTopology {
2369    fn default() -> Self {
2370        Self {
2371            vp_count: 2,
2372            enable_smt: None,
2373            vps_per_socket: None,
2374            apic_mode: None,
2375        }
2376    }
2377}
2378
2379impl ProcessorTopology {
2380    /// A large number of VPs
2381    pub fn heavy() -> Self {
2382        Self {
2383            vp_count: 16,
2384            vps_per_socket: Some(8),
2385            ..Default::default()
2386        }
2387    }
2388
2389    /// A very large number of VPs
2390    pub fn very_heavy() -> Self {
2391        Self {
2392            vp_count: 32,
2393            vps_per_socket: Some(16),
2394            ..Default::default()
2395        }
2396    }
2397}
2398
2399/// The APIC mode for the VM.
2400#[derive(Debug, Clone, Copy)]
2401pub enum ApicMode {
2402    /// xAPIC mode only.
2403    Xapic,
2404    /// x2APIC mode supported but not enabled at boot.
2405    X2apicSupported,
2406    /// x2APIC mode enabled at boot.
2407    X2apicEnabled,
2408}
2409
2410/// Common memory configuration information for the VM.
2411#[derive(Debug)]
2412pub struct MemoryConfig {
2413    /// Specifies the amount of memory, in bytes, to assign to the
2414    /// virtual machine.
2415    pub startup_bytes: u64,
2416    /// Specifies the minimum and maximum amount of dynamic memory, in bytes.
2417    ///
2418    /// Dynamic memory will be disabled if this is `None`.
2419    pub dynamic_memory_range: Option<(u64, u64)>,
2420    /// Per-NUMA-node memory sizes. When set, RAM is distributed across
2421    /// vNUMA nodes instead of assigning all RAM to node 0.
2422    pub numa_mem_sizes: Option<Vec<u64>>,
2423    /// Whether to back guest RAM with private anonymous memory rather than a
2424    /// shared (file/memfd-backed) memory section.
2425    ///
2426    /// - `None` (the default) uses private memory whenever the configuration
2427    ///   allows it, falling back to shared memory otherwise. Private anonymous
2428    ///   memory is cheaper to set up and eligible for Transparent Huge Pages,
2429    ///   so it is preferred for performance; this lets each VM get the best
2430    ///   backing for its firmware without the test having to know the details.
2431    /// - `Some(true)` explicitly requires private memory. This is an error if
2432    ///   the configuration is incompatible with private memory (OpenHCL, which
2433    ///   shares VTL0 RAM with VTL2 via a remote mapper, and PCAT/Gen1, which
2434    ///   relies on x86 legacy support, both require shared memory), rather than
2435    ///   silently downgrading to shared.
2436    /// - `Some(false)` explicitly requires shared memory, for tests that need
2437    ///   the guest RAM backing to be shareable with another process, such as
2438    ///   vhost-user backends.
2439    ///
2440    /// Only applies to the OpenVMM backend; ignored by Hyper-V.
2441    pub private_memory: Option<bool>,
2442    /// Mark guest RAM as eligible for Transparent Huge Pages (THP),
2443    /// improving performance for large allocations.
2444    ///
2445    /// Defaults to `true`. Applies to private anonymous guest RAM and to
2446    /// shared memfd-backed RAM, on Linux (via `madvise`) and on Windows (via
2447    /// soft large pages). It has no effect on explicit hugetlb/large-page
2448    /// backings (see
2449    /// [`with_hugepages`](crate::openvmm::PetriVmConfigOpenVmm::with_hugepages)),
2450    /// which are already huge.
2451    ///
2452    /// Only applies to the OpenVMM backend; ignored by Hyper-V.
2453    pub transparent_hugepages: bool,
2454}
2455
2456impl Default for MemoryConfig {
2457    fn default() -> Self {
2458        Self {
2459            startup_bytes: 4 * 1024 * 1024 * 1024, // 4 GiB
2460            dynamic_memory_range: None,
2461            numa_mem_sizes: None,
2462            private_memory: None,
2463            transparent_hugepages: true,
2464        }
2465    }
2466}
2467
2468/// UEFI firmware configuration
2469#[derive(Debug)]
2470pub struct UefiConfig {
2471    /// Enable secure boot
2472    pub secure_boot_enabled: bool,
2473    /// Secure boot template
2474    pub secure_boot_template: Option<SecureBootTemplate>,
2475    /// Custom UEFI variable delta JSON
2476    pub custom_uefi_json: Option<Vec<u8>>,
2477    /// Disable the UEFI frontpage which will cause the VM to shutdown instead when unable to boot.
2478    pub disable_frontpage: bool,
2479    /// Always attempt a default boot
2480    pub default_boot_always_attempt: bool,
2481    /// Enable vPCI boot (for NVMe)
2482    pub enable_vpci_boot: bool,
2483    /// Force UEFI to bounce-buffer all DMA traffic
2484    pub force_dma_bounce: bool,
2485    /// EFI diagnostics log level filter
2486    pub efi_diagnostics_log_level: EfiDiagnosticsLogLevel,
2487    /// Per-period rate-limit override for EFI diagnostics emission.
2488    /// See [`PetriVmBuilder::with_efi_diagnostics_rate_limit()`] for more information.
2489    pub efi_diagnostics_rate_limit: Option<u32>,
2490}
2491
2492impl Default for UefiConfig {
2493    fn default() -> Self {
2494        Self {
2495            secure_boot_enabled: false,
2496            secure_boot_template: None,
2497            custom_uefi_json: None,
2498            disable_frontpage: true,
2499            default_boot_always_attempt: false,
2500            enable_vpci_boot: false,
2501            force_dma_bounce: false,
2502            efi_diagnostics_log_level: EfiDiagnosticsLogLevel::Default,
2503            efi_diagnostics_rate_limit: None,
2504        }
2505    }
2506}
2507
2508/// EFI diagnostics log level filter.
2509///
2510/// Controls which UEFI diagnostics log entries are forwarded to the host
2511/// tracing infrastructure (and thus visible via kmsg / test output).
2512#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2513pub enum EfiDiagnosticsLogLevel {
2514    /// Default log level (ERROR and WARN only).
2515    #[default]
2516    Default,
2517    /// Include INFO logs (ERROR, WARN, and INFO).
2518    Info,
2519    /// All log levels.
2520    Full,
2521}
2522
2523/// Control the logging configuration of OpenVMM/OpenHCL.
2524#[derive(Debug, Clone)]
2525pub enum OpenvmmLogConfig {
2526    /// Use the default log levels used by petri tests. This will forward
2527    /// `OPENVMM_LOG` and `OPENVMM_SHOW_SPANS` from the environment if they are
2528    /// set, otherwise it will use `debug` and `true` respectively
2529    TestDefault,
2530    /// Use the built-in default log levels of OpenHCL/OpenVMM (e.g. don't pass
2531    /// OPENVMM_LOG or OPENVMM_SHOW_SPANS)
2532    BuiltInDefault,
2533    /// Use the provided custom log levels, specified as key/value pairs. At this time,
2534    /// simply uses the already-defined environment variables (e.g.
2535    /// `OPENVMM_LOG=info,disk_nvme=debug OPENVMM_SHOW_SPANS=true`)
2536    ///
2537    /// See the Guide and source code for configuring these logs.
2538    /// - For the host VMM: see `enable_tracing` in `tracing_init.rs` for details on
2539    ///   the accepted keys and values.
2540    /// - For OpenHCL, see `init_tracing_backend` in `openhcl/src/logging/mod.rs` for details on
2541    ///   the accepted keys and values.
2542    Custom(BTreeMap<String, String>),
2543}
2544
2545/// OpenHCL configuration
2546#[derive(Debug)]
2547pub struct OpenHclConfig {
2548    /// Whether to enable VMBus redirection
2549    pub vmbus_redirect: bool,
2550    /// Whether to enable MANA keepalive at boot.
2551    pub enable_mana_keepalive: bool,
2552    /// Test-specified command-line parameters to append to the petri generated
2553    /// command line and pass to OpenHCL. VM backends should use
2554    /// [`OpenHclConfig::command_line()`] rather than reading this directly.
2555    pub custom_command_line: Option<String>,
2556    /// Command line parameters that control OpenHCL logging behavior. Separate
2557    /// from `command_line` so that petri can decide to use default log
2558    /// levels.
2559    pub log_levels: OpenvmmLogConfig,
2560    /// How to place VTL2 in address space. If `None`, the backend VMM
2561    /// will decide on default behavior.
2562    pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
2563    /// VTL2 settings
2564    pub vtl2_settings: Option<Vtl2Settings>,
2565}
2566
2567impl OpenHclConfig {
2568    /// Returns the command line to pass to OpenHCL based on these parameters. Aggregates
2569    /// the command line and log levels.
2570    pub fn command_line(&self) -> String {
2571        let mut cmdline = self.custom_command_line.clone();
2572
2573        if self.enable_mana_keepalive {
2574            append_cmdline(&mut cmdline, "OPENHCL_MANA_KEEP_ALIVE=host,privatepool");
2575        }
2576
2577        match &self.log_levels {
2578            OpenvmmLogConfig::TestDefault => {
2579                let default_log_levels = {
2580                    // Forward OPENVMM_LOG and OPENVMM_SHOW_SPANS to OpenHCL if they're set.
2581                    let openhcl_tracing = if let Ok(x) =
2582                        std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG"))
2583                    {
2584                        format!("OPENVMM_LOG={x}")
2585                    } else {
2586                        "OPENVMM_LOG=debug".to_owned()
2587                    };
2588                    let openhcl_show_spans = if let Ok(x) = std::env::var("OPENVMM_SHOW_SPANS") {
2589                        format!("OPENVMM_SHOW_SPANS={x}")
2590                    } else {
2591                        "OPENVMM_SHOW_SPANS=true".to_owned()
2592                    };
2593                    format!("{openhcl_tracing} {openhcl_show_spans}")
2594                };
2595                append_cmdline(&mut cmdline, &default_log_levels);
2596            }
2597            OpenvmmLogConfig::BuiltInDefault => {
2598                // do nothing, use whatever the built-in default is
2599            }
2600            OpenvmmLogConfig::Custom(levels) => {
2601                levels.iter().for_each(|(key, value)| {
2602                    append_cmdline(&mut cmdline, format!("{key}={value}"));
2603                });
2604            }
2605        }
2606
2607        cmdline.unwrap_or_default()
2608    }
2609}
2610
2611impl Default for OpenHclConfig {
2612    fn default() -> Self {
2613        Self {
2614            vmbus_redirect: false,
2615            enable_mana_keepalive: true,
2616            custom_command_line: None,
2617            log_levels: OpenvmmLogConfig::TestDefault,
2618            vtl2_base_address_type: None,
2619            vtl2_settings: None,
2620        }
2621    }
2622}
2623
2624/// TPM configuration
2625#[derive(Debug)]
2626pub struct TpmConfig {
2627    /// Use ephemeral TPM state (do not persist to VMGS)
2628    pub no_persistent_secrets: bool,
2629    /// Hardware sealing policy for sealed secrets
2630    pub hardware_sealing_policy: PetriHardwareSealingPolicy,
2631    /// TPM reference implementation version
2632    pub version: PetriTpmVersion,
2633}
2634
2635impl Default for TpmConfig {
2636    fn default() -> Self {
2637        Self {
2638            no_persistent_secrets: true,
2639            hardware_sealing_policy: PetriHardwareSealingPolicy::Default,
2640            version: PetriTpmVersion::default(),
2641        }
2642    }
2643}
2644
2645/// TPM reference implementation version used by the test infrastructure.
2646#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2647pub enum PetriTpmVersion {
2648    /// TPM reference implementation version 1.38
2649    V138,
2650    /// TPM reference implementation version 1.85
2651    #[default]
2652    V185,
2653}
2654
2655impl From<PetriTpmVersion> for tpm_resources::TpmVersion {
2656    fn from(version: PetriTpmVersion) -> Self {
2657        match version {
2658            PetriTpmVersion::V138 => tpm_resources::TpmVersion::V138,
2659            PetriTpmVersion::V185 => tpm_resources::TpmVersion::V185,
2660        }
2661    }
2662}
2663
2664impl From<PetriTpmVersion> for get_resources::ged::GedTpmVersion {
2665    fn from(version: PetriTpmVersion) -> Self {
2666        match version {
2667            PetriTpmVersion::V138 => get_resources::ged::GedTpmVersion::V138,
2668            PetriTpmVersion::V185 => get_resources::ged::GedTpmVersion::V185,
2669        }
2670    }
2671}
2672
2673/// Hardware sealing policy used by the test infrastructure.
2674///
2675/// Maps to Hyper-V `Set-GuestStateEncryptionPolicy` values and
2676/// underhill's `HardwareSealingPolicy`.
2677#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2678pub enum PetriHardwareSealingPolicy {
2679    /// No explicit policy — the backend picks its default.
2680    #[default]
2681    Default,
2682    /// Derive the hardware sealing key from measurement hash.
2683    HashPolicy,
2684    /// Derive the hardware sealing key from signer information.
2685    SignerPolicy,
2686}
2687
2688/// Firmware to load into the test VM.
2689// TODO: remove the guests from the firmware enum so that we don't pass them
2690// to the VMM backend after we have already used them generically.
2691#[derive(Debug)]
2692pub enum Firmware {
2693    /// Boot Linux directly, without any firmware.
2694    LinuxDirect {
2695        /// The kernel to boot.
2696        kernel: ResolvedArtifact,
2697        /// The initrd to use.
2698        initrd: ResolvedArtifact,
2699    },
2700    /// Boot Linux directly, without any firmware, with OpenHCL in VTL2.
2701    OpenhclLinuxDirect {
2702        /// The path to the IGVM file to use.
2703        igvm_path: ResolvedArtifact,
2704        /// OpenHCL configuration
2705        openhcl_config: OpenHclConfig,
2706    },
2707    /// Boot a PCAT-based VM.
2708    Pcat {
2709        /// The guest OS the VM will boot into.
2710        guest: PcatGuest,
2711        /// The firmware to use.
2712        bios_firmware: ResolvedOptionalArtifact,
2713        /// The SVGA firmware to use.
2714        svga_firmware: ResolvedOptionalArtifact,
2715        /// IDE controllers and associated disks
2716        ide_controllers: [[Option<Drive>; 2]; 2],
2717    },
2718    /// Boot a PCAT-based VM with OpenHCL in VTL2.
2719    OpenhclPcat {
2720        /// The guest OS the VM will boot into.
2721        guest: PcatGuest,
2722        /// The path to the IGVM file to use.
2723        igvm_path: ResolvedArtifact,
2724        /// The firmware to use.
2725        bios_firmware: ResolvedOptionalArtifact,
2726        /// The SVGA firmware to use.
2727        svga_firmware: ResolvedOptionalArtifact,
2728        /// OpenHCL configuration
2729        openhcl_config: OpenHclConfig,
2730    },
2731    /// Boot a UEFI-based VM.
2732    Uefi {
2733        /// The guest OS the VM will boot into.
2734        guest: UefiGuest,
2735        /// The firmware to use.
2736        uefi_firmware: ResolvedArtifact,
2737        /// UEFI configuration
2738        uefi_config: UefiConfig,
2739    },
2740    /// Boot a UEFI-based VM with OpenHCL in VTL2.
2741    OpenhclUefi {
2742        /// The guest OS the VM will boot into.
2743        guest: UefiGuest,
2744        /// The isolation type of the VM.
2745        isolation: Option<IsolationType>,
2746        /// The path to the IGVM file to use.
2747        igvm_path: ResolvedArtifact,
2748        /// UEFI configuration
2749        uefi_config: UefiConfig,
2750        /// OpenHCL configuration
2751        openhcl_config: OpenHclConfig,
2752    },
2753}
2754
2755/// The boot device type.
2756#[derive(Debug, Copy, Clone, PartialEq, Eq)]
2757pub enum BootDeviceType {
2758    /// Don't initialize a boot device.
2759    None,
2760    /// Boot from IDE.
2761    Ide,
2762    /// Boot from IDE via SCSI to VTL2.
2763    IdeViaScsi,
2764    /// Boot from IDE via NVME to VTL2.
2765    IdeViaNvme,
2766    /// Boot from SCSI.
2767    Scsi,
2768    /// Boot from SCSI via SCSI to VTL2.
2769    ScsiViaScsi,
2770    /// Boot from SCSI via NVME to VTL2.
2771    ScsiViaNvme,
2772    /// Boot from NVMe.
2773    Nvme,
2774    /// Boot from NVMe via SCSI to VTL2.
2775    NvmeViaScsi,
2776    /// Boot from NVMe via NVMe to VTL2.
2777    NvmeViaNvme,
2778    /// Boot from NVMe attached to a PCIe root port.
2779    PcieNvme,
2780    /// Boot from virtio-blk attached to a PCIe root port.
2781    PcieVirtioBlk,
2782}
2783
2784impl BootDeviceType {
2785    fn requires_vtl2(&self) -> bool {
2786        match self {
2787            BootDeviceType::None
2788            | BootDeviceType::Ide
2789            | BootDeviceType::Scsi
2790            | BootDeviceType::Nvme
2791            | BootDeviceType::PcieNvme
2792            | BootDeviceType::PcieVirtioBlk => false,
2793            BootDeviceType::IdeViaScsi
2794            | BootDeviceType::IdeViaNvme
2795            | BootDeviceType::ScsiViaScsi
2796            | BootDeviceType::ScsiViaNvme
2797            | BootDeviceType::NvmeViaScsi
2798            | BootDeviceType::NvmeViaNvme => true,
2799        }
2800    }
2801
2802    fn requires_vpci_boot(&self) -> bool {
2803        matches!(
2804            self,
2805            BootDeviceType::Nvme | BootDeviceType::NvmeViaScsi | BootDeviceType::NvmeViaNvme
2806        )
2807    }
2808
2809    fn requires_vmbus(&self) -> bool {
2810        match self {
2811            BootDeviceType::None
2812            | BootDeviceType::Ide
2813            | BootDeviceType::PcieNvme
2814            | BootDeviceType::PcieVirtioBlk => false,
2815            BootDeviceType::IdeViaScsi
2816            | BootDeviceType::IdeViaNvme
2817            | BootDeviceType::Scsi
2818            | BootDeviceType::ScsiViaScsi
2819            | BootDeviceType::ScsiViaNvme
2820            | BootDeviceType::Nvme
2821            | BootDeviceType::NvmeViaScsi
2822            | BootDeviceType::NvmeViaNvme => true,
2823        }
2824    }
2825}
2826
2827impl Firmware {
2828    /// Constructs a standard [`Firmware::LinuxDirect`] configuration.
2829    pub fn linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2830        use petri_artifacts_vmm_test::artifacts::loadable::*;
2831        match arch {
2832            MachineArch::X86_64 => Firmware::LinuxDirect {
2833                kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_X64).erase(),
2834                initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
2835            },
2836            MachineArch::Aarch64 => Firmware::LinuxDirect {
2837                kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_AARCH64).erase(),
2838                initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_AARCH64).erase(),
2839            },
2840        }
2841    }
2842
2843    /// Constructs a [`Firmware::LinuxDirect`] configuration that uses a
2844    /// compressed bzImage kernel instead of an uncompressed ELF.
2845    ///
2846    /// This is x86_64-only, as bzImage is an x86-specific format.
2847    pub fn linux_direct_bzimage(resolver: &ArtifactResolver<'_>) -> Self {
2848        use petri_artifacts_vmm_test::artifacts::loadable::*;
2849        Firmware::LinuxDirect {
2850            kernel: resolver.require(LINUX_DIRECT_TEST_BZIMAGE_X64).erase(),
2851            initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
2852        }
2853    }
2854
2855    /// Constructs a standard [`Firmware::OpenhclLinuxDirect`] configuration.
2856    pub fn openhcl_linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2857        use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2858        match arch {
2859            MachineArch::X86_64 => Firmware::OpenhclLinuxDirect {
2860                igvm_path: resolver.require(LATEST_LINUX_DIRECT_TEST_X64).erase(),
2861                openhcl_config: Default::default(),
2862            },
2863            MachineArch::Aarch64 => todo!("Linux direct not yet supported on aarch64"),
2864        }
2865    }
2866
2867    /// Constructs a standard [`Firmware::Pcat`] configuration.
2868    pub fn pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2869        use petri_artifacts_vmm_test::artifacts::loadable::*;
2870        Firmware::Pcat {
2871            guest,
2872            bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2873            svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2874            ide_controllers: [[None, None], [None, None]],
2875        }
2876    }
2877
2878    /// Constructs a standard [`Firmware::OpenhclPcat`] configuration.
2879    pub fn openhcl_pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2880        use petri_artifacts_vmm_test::artifacts::loadable::*;
2881        use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2882        Firmware::OpenhclPcat {
2883            guest,
2884            igvm_path: resolver.require(LATEST_STANDARD_X64).erase(),
2885            bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2886            svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2887            openhcl_config: OpenHclConfig {
2888                // VMBUS redirect is necessary for IDE to be provided by VTL2
2889                vmbus_redirect: true,
2890                ..Default::default()
2891            },
2892        }
2893    }
2894
2895    /// Constructs a standard [`Firmware::Uefi`] configuration.
2896    pub fn uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch, guest: UefiGuest) -> Self {
2897        use petri_artifacts_vmm_test::artifacts::loadable::*;
2898        let uefi_firmware = match arch {
2899            MachineArch::X86_64 => resolver.require(UEFI_FIRMWARE_X64).erase(),
2900            MachineArch::Aarch64 => resolver.require(UEFI_FIRMWARE_AARCH64).erase(),
2901        };
2902        Firmware::Uefi {
2903            guest,
2904            uefi_firmware,
2905            uefi_config: Default::default(),
2906        }
2907    }
2908
2909    /// Constructs a standard [`Firmware::OpenhclUefi`] configuration.
2910    pub fn openhcl_uefi(
2911        resolver: &ArtifactResolver<'_>,
2912        arch: MachineArch,
2913        guest: UefiGuest,
2914        isolation: Option<IsolationType>,
2915    ) -> Self {
2916        use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2917        let igvm_path = match arch {
2918            MachineArch::X86_64 if isolation.is_some() => resolver.require(LATEST_CVM_X64).erase(),
2919            MachineArch::X86_64 => resolver.require(LATEST_STANDARD_X64).erase(),
2920            MachineArch::Aarch64 => resolver.require(LATEST_STANDARD_AARCH64).erase(),
2921        };
2922        Firmware::OpenhclUefi {
2923            guest,
2924            isolation,
2925            igvm_path,
2926            uefi_config: Default::default(),
2927            openhcl_config: Default::default(),
2928        }
2929    }
2930
2931    fn is_openhcl(&self) -> bool {
2932        match self {
2933            Firmware::OpenhclLinuxDirect { .. }
2934            | Firmware::OpenhclUefi { .. }
2935            | Firmware::OpenhclPcat { .. } => true,
2936            Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => false,
2937        }
2938    }
2939
2940    fn isolation(&self) -> Option<IsolationType> {
2941        match self {
2942            Firmware::OpenhclUefi { isolation, .. } => *isolation,
2943            Firmware::LinuxDirect { .. }
2944            | Firmware::Pcat { .. }
2945            | Firmware::Uefi { .. }
2946            | Firmware::OpenhclLinuxDirect { .. }
2947            | Firmware::OpenhclPcat { .. } => None,
2948        }
2949    }
2950
2951    fn is_linux_direct(&self) -> bool {
2952        match self {
2953            Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => true,
2954            Firmware::Pcat { .. }
2955            | Firmware::Uefi { .. }
2956            | Firmware::OpenhclUefi { .. }
2957            | Firmware::OpenhclPcat { .. } => false,
2958        }
2959    }
2960
2961    /// Get the initrd path for Linux direct boot firmware.
2962    pub fn linux_direct_initrd(&self) -> Option<&Path> {
2963        match self {
2964            Firmware::LinuxDirect { initrd, .. } => Some(initrd.get()),
2965            _ => None,
2966        }
2967    }
2968
2969    fn is_pcat(&self) -> bool {
2970        match self {
2971            Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => true,
2972            Firmware::Uefi { .. }
2973            | Firmware::OpenhclUefi { .. }
2974            | Firmware::LinuxDirect { .. }
2975            | Firmware::OpenhclLinuxDirect { .. } => false,
2976        }
2977    }
2978
2979    fn os_flavor(&self) -> OsFlavor {
2980        match self {
2981            Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => OsFlavor::Linux,
2982            Firmware::Uefi {
2983                guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2984                ..
2985            }
2986            | Firmware::OpenhclUefi {
2987                guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2988                ..
2989            } => OsFlavor::Uefi,
2990            Firmware::Pcat {
2991                guest: PcatGuest::Vhd(cfg),
2992                ..
2993            }
2994            | Firmware::OpenhclPcat {
2995                guest: PcatGuest::Vhd(cfg),
2996                ..
2997            }
2998            | Firmware::Uefi {
2999                guest: UefiGuest::Vhd(cfg),
3000                ..
3001            }
3002            | Firmware::OpenhclUefi {
3003                guest: UefiGuest::Vhd(cfg),
3004                ..
3005            } => cfg.os_flavor,
3006            Firmware::Pcat {
3007                guest: PcatGuest::Iso(cfg),
3008                ..
3009            }
3010            | Firmware::OpenhclPcat {
3011                guest: PcatGuest::Iso(cfg),
3012                ..
3013            } => cfg.os_flavor,
3014        }
3015    }
3016
3017    fn quirks(&self) -> GuestQuirks {
3018        match self {
3019            Firmware::Pcat {
3020                guest: PcatGuest::Vhd(cfg),
3021                ..
3022            }
3023            | Firmware::Uefi {
3024                guest: UefiGuest::Vhd(cfg),
3025                ..
3026            }
3027            | Firmware::OpenhclUefi {
3028                guest: UefiGuest::Vhd(cfg),
3029                ..
3030            } => cfg.quirks.clone(),
3031            Firmware::Pcat {
3032                guest: PcatGuest::Iso(cfg),
3033                ..
3034            } => cfg.quirks.clone(),
3035            _ => Default::default(),
3036        }
3037    }
3038
3039    fn expected_boot_event(&self) -> Option<FirmwareEvent> {
3040        match self {
3041            Firmware::LinuxDirect { .. }
3042            | Firmware::OpenhclLinuxDirect { .. }
3043            | Firmware::Uefi {
3044                guest: UefiGuest::GuestTestUefi(_),
3045                ..
3046            }
3047            | Firmware::OpenhclUefi {
3048                guest: UefiGuest::GuestTestUefi(_),
3049                ..
3050            } => None,
3051            Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => {
3052                // TODO: Handle older PCAT versions that don't fire the event
3053                Some(FirmwareEvent::BootAttempt)
3054            }
3055            Firmware::Uefi {
3056                guest: UefiGuest::None,
3057                ..
3058            }
3059            | Firmware::OpenhclUefi {
3060                guest: UefiGuest::None,
3061                ..
3062            } => Some(FirmwareEvent::NoBootDevice),
3063            Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => {
3064                Some(FirmwareEvent::BootSuccess)
3065            }
3066        }
3067    }
3068
3069    fn openhcl_config(&self) -> Option<&OpenHclConfig> {
3070        match self {
3071            Firmware::OpenhclLinuxDirect { openhcl_config, .. }
3072            | Firmware::OpenhclUefi { openhcl_config, .. }
3073            | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
3074            Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
3075        }
3076    }
3077
3078    fn openhcl_config_mut(&mut self) -> Option<&mut OpenHclConfig> {
3079        match self {
3080            Firmware::OpenhclLinuxDirect { openhcl_config, .. }
3081            | Firmware::OpenhclUefi { openhcl_config, .. }
3082            | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
3083            Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
3084        }
3085    }
3086
3087    #[cfg_attr(not(windows), expect(dead_code))]
3088    fn openhcl_firmware(&self) -> Option<&Path> {
3089        match self {
3090            Firmware::OpenhclLinuxDirect { igvm_path, .. }
3091            | Firmware::OpenhclUefi { igvm_path, .. }
3092            | Firmware::OpenhclPcat { igvm_path, .. } => Some(igvm_path.get()),
3093            Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
3094        }
3095    }
3096
3097    fn into_runtime_config(
3098        self,
3099        vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
3100    ) -> PetriVmRuntimeConfig {
3101        match self {
3102            Firmware::OpenhclLinuxDirect { openhcl_config, .. }
3103            | Firmware::OpenhclUefi { openhcl_config, .. }
3104            | Firmware::OpenhclPcat { openhcl_config, .. } => PetriVmRuntimeConfig {
3105                vtl2_settings: Some(
3106                    openhcl_config
3107                        .vtl2_settings
3108                        .unwrap_or_else(default_vtl2_settings),
3109                ),
3110                ide_controllers: None,
3111                vmbus_storage_controllers,
3112            },
3113            Firmware::Pcat {
3114                ide_controllers, ..
3115            } => PetriVmRuntimeConfig {
3116                vtl2_settings: None,
3117                ide_controllers: Some(ide_controllers),
3118                vmbus_storage_controllers,
3119            },
3120            Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } => PetriVmRuntimeConfig {
3121                vtl2_settings: None,
3122                ide_controllers: None,
3123                vmbus_storage_controllers,
3124            },
3125        }
3126    }
3127
3128    fn uefi_config(&self) -> Option<&UefiConfig> {
3129        match self {
3130            Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
3131                Some(uefi_config)
3132            }
3133            Firmware::LinuxDirect { .. }
3134            | Firmware::OpenhclLinuxDirect { .. }
3135            | Firmware::Pcat { .. }
3136            | Firmware::OpenhclPcat { .. } => None,
3137        }
3138    }
3139
3140    fn uefi_config_mut(&mut self) -> Option<&mut UefiConfig> {
3141        match self {
3142            Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
3143                Some(uefi_config)
3144            }
3145            Firmware::LinuxDirect { .. }
3146            | Firmware::OpenhclLinuxDirect { .. }
3147            | Firmware::Pcat { .. }
3148            | Firmware::OpenhclPcat { .. } => None,
3149        }
3150    }
3151
3152    fn boot_drive(&self) -> Option<Drive> {
3153        match self {
3154            Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => None,
3155            Firmware::Pcat { guest, .. } | Firmware::OpenhclPcat { guest, .. } => {
3156                Some((guest.disk_path(), guest.is_dvd()))
3157            }
3158            Firmware::Uefi { guest, .. } | Firmware::OpenhclUefi { guest, .. } => {
3159                guest.disk_path().map(|dp| (dp, false))
3160            }
3161        }
3162        .map(|(disk_path, is_dvd)| Drive::new(Some(Disk::Differencing(disk_path)), is_dvd))
3163    }
3164
3165    fn vtl2_settings(&mut self) -> Option<&mut Vtl2Settings> {
3166        self.openhcl_config_mut()
3167            .map(|c| c.vtl2_settings.get_or_insert_with(default_vtl2_settings))
3168    }
3169
3170    fn ide_controllers(&self) -> Option<&[[Option<Drive>; 2]; 2]> {
3171        match self {
3172            Firmware::Pcat {
3173                ide_controllers, ..
3174            } => Some(ide_controllers),
3175            _ => None,
3176        }
3177    }
3178
3179    fn ide_controllers_mut(&mut self) -> Option<&mut [[Option<Drive>; 2]; 2]> {
3180        match self {
3181            Firmware::Pcat {
3182                ide_controllers, ..
3183            } => Some(ide_controllers),
3184            _ => None,
3185        }
3186    }
3187}
3188
3189/// The guest the VM will boot into. A boot drive with the chosen setup
3190/// will be automatically configured.
3191#[derive(Debug)]
3192pub enum PcatGuest {
3193    /// Mount a VHD as the boot drive.
3194    Vhd(BootImageConfig<boot_image_type::Vhd>),
3195    /// Mount an ISO as the CD/DVD drive.
3196    Iso(BootImageConfig<boot_image_type::Iso>),
3197}
3198
3199impl PcatGuest {
3200    fn disk_path(&self) -> DiskPath {
3201        match self {
3202            PcatGuest::Vhd(disk) => disk.disk_path(),
3203            PcatGuest::Iso(disk) => disk.disk_path(),
3204        }
3205    }
3206
3207    fn is_dvd(&self) -> bool {
3208        matches!(self, Self::Iso(_))
3209    }
3210}
3211
3212/// The guest the VM will boot into. A boot drive with the chosen setup
3213/// will be automatically configured.
3214#[derive(Debug)]
3215pub enum UefiGuest {
3216    /// Mount a VHD as the boot drive.
3217    Vhd(BootImageConfig<boot_image_type::Vhd>),
3218    /// The UEFI test image produced by our guest-test infrastructure.
3219    GuestTestUefi(ResolvedArtifact),
3220    /// No guest, just the firmware.
3221    None,
3222}
3223
3224impl UefiGuest {
3225    /// Construct a standard [`UefiGuest::GuestTestUefi`] configuration.
3226    pub fn guest_test_uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
3227        use petri_artifacts_vmm_test::artifacts::test_vhd::*;
3228        let artifact = match arch {
3229            MachineArch::X86_64 => resolver.require(GUEST_TEST_UEFI_X64).erase(),
3230            MachineArch::Aarch64 => resolver.require(GUEST_TEST_UEFI_AARCH64).erase(),
3231        };
3232        UefiGuest::GuestTestUefi(artifact)
3233    }
3234
3235    fn disk_path(&self) -> Option<DiskPath> {
3236        match self {
3237            UefiGuest::Vhd(vhd) => Some(vhd.disk_path()),
3238            UefiGuest::GuestTestUefi(p) => Some(DiskPath::Local(p.get().to_path_buf())),
3239            UefiGuest::None => None,
3240        }
3241    }
3242}
3243
3244/// Type-tags for [`BootImageConfig`](super::BootImageConfig)
3245pub mod boot_image_type {
3246    mod private {
3247        pub trait Sealed {}
3248        impl Sealed for super::Vhd {}
3249        impl Sealed for super::Iso {}
3250    }
3251
3252    /// Private trait use to seal the set of artifact types BootImageType
3253    /// supports.
3254    pub trait BootImageType: private::Sealed {}
3255
3256    /// BootImageConfig for a VHD file
3257    #[derive(Debug)]
3258    pub enum Vhd {}
3259
3260    /// BootImageConfig for an ISO file
3261    #[derive(Debug)]
3262    pub enum Iso {}
3263
3264    impl BootImageType for Vhd {}
3265    impl BootImageType for Iso {}
3266}
3267
3268/// Configuration information for the boot drive of the VM.
3269#[derive(Debug)]
3270pub struct BootImageConfig<T: boot_image_type::BootImageType> {
3271    /// Artifact source corresponding to the boot media (local or remote).
3272    artifact: ResolvedArtifactSource,
3273    /// The OS flavor.
3274    os_flavor: OsFlavor,
3275    /// Any quirks needed to boot the guest.
3276    ///
3277    /// Most guests should not need any quirks, and can use `Default`.
3278    quirks: GuestQuirks,
3279    /// Marker denoting what type of media `artifact` corresponds to
3280    _type: core::marker::PhantomData<T>,
3281}
3282
3283impl<T: boot_image_type::BootImageType> BootImageConfig<T> {
3284    /// Get a [`DiskPath`] from the artifact source.
3285    fn disk_path(&self) -> DiskPath {
3286        match self.artifact.get() {
3287            ArtifactSource::Local(p) => DiskPath::Local(p.clone()),
3288            ArtifactSource::Remote { url } => DiskPath::Remote { url: url.clone() },
3289        }
3290    }
3291}
3292
3293impl BootImageConfig<boot_image_type::Vhd> {
3294    /// Create a new BootImageConfig from a VHD artifact source
3295    pub fn from_vhd<A>(artifact: ResolvedArtifactSource<A>) -> Self
3296    where
3297        A: petri_artifacts_common::tags::IsTestVhd,
3298    {
3299        BootImageConfig {
3300            artifact: artifact.erase(),
3301            os_flavor: A::OS_FLAVOR,
3302            quirks: A::quirks(),
3303            _type: std::marker::PhantomData,
3304        }
3305    }
3306}
3307
3308impl BootImageConfig<boot_image_type::Iso> {
3309    /// Create a new BootImageConfig from an ISO artifact source
3310    pub fn from_iso<A>(artifact: ResolvedArtifactSource<A>) -> Self
3311    where
3312        A: petri_artifacts_common::tags::IsTestIso,
3313    {
3314        BootImageConfig {
3315            artifact: artifact.erase(),
3316            os_flavor: A::OS_FLAVOR,
3317            quirks: A::quirks(),
3318            _type: std::marker::PhantomData,
3319        }
3320    }
3321}
3322
3323/// Isolation type
3324#[derive(Debug, Clone, Copy)]
3325pub enum IsolationType {
3326    /// VBS
3327    Vbs,
3328    /// SNP
3329    Snp,
3330    /// TDX
3331    Tdx,
3332}
3333
3334/// Flags controlling servicing behavior.
3335#[derive(Debug, Clone, Copy)]
3336pub struct OpenHclServicingFlags {
3337    /// Preserve DMA memory for NVMe devices if supported.
3338    /// Defaults to `true`.
3339    pub enable_nvme_keepalive: bool,
3340    /// Preserve DMA memory for MANA devices if supported.
3341    pub enable_mana_keepalive: bool,
3342    /// 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.
3343    pub override_version_checks: bool,
3344    /// Hint to the OpenHCL runtime how much time to wait when stopping / saving the OpenHCL.
3345    pub stop_timeout_hint_secs: Option<u16>,
3346}
3347
3348/// Where a disk image is located.
3349#[derive(Debug, Clone)]
3350pub enum DiskPath {
3351    /// A local file path.
3352    Local(PathBuf),
3353    /// A remote URL (fetched on demand via HTTP Range requests).
3354    Remote {
3355        /// The URL where the disk can be fetched.
3356        url: String,
3357    },
3358}
3359
3360impl From<PathBuf> for DiskPath {
3361    fn from(path: PathBuf) -> Self {
3362        DiskPath::Local(path)
3363    }
3364}
3365
3366/// Petri disk
3367#[derive(Debug, Clone)]
3368pub enum Disk {
3369    /// Memory backed with specified size
3370    Memory(u64),
3371    /// Memory differencing disk backed by a VHD (local or remote)
3372    Differencing(DiskPath),
3373    /// Persistent VHD
3374    Persistent(PathBuf),
3375    /// Disk backed by a temporary VHD
3376    Temporary(Arc<TempPath>),
3377}
3378
3379/// Petri VMGS disk
3380#[derive(Debug, Clone)]
3381pub struct PetriVmgsDisk {
3382    /// Backing disk
3383    pub disk: Disk,
3384    /// Guest state encryption policy
3385    pub encryption_policy: GuestStateEncryptionPolicy,
3386}
3387
3388impl Default for PetriVmgsDisk {
3389    fn default() -> Self {
3390        PetriVmgsDisk {
3391            disk: Disk::Memory(vmgs_format::VMGS_DEFAULT_CAPACITY),
3392            // TODO: make this strict once we can set it in OpenHCL on Hyper-V
3393            encryption_policy: GuestStateEncryptionPolicy::None(false),
3394        }
3395    }
3396}
3397
3398/// Petri VM guest state resource
3399#[derive(Debug, Clone)]
3400pub enum PetriVmgsResource {
3401    /// Use disk to store guest state
3402    Disk(PetriVmgsDisk),
3403    /// Use disk to store guest state, reformatting if corrupted.
3404    ReprovisionOnFailure(PetriVmgsDisk),
3405    /// Format and use disk to store guest state
3406    Reprovision(PetriVmgsDisk),
3407    /// Store guest state in memory
3408    Ephemeral,
3409}
3410
3411impl PetriVmgsResource {
3412    /// get the inner vmgs disk if one exists
3413    pub fn vmgs(&self) -> Option<&PetriVmgsDisk> {
3414        match self {
3415            PetriVmgsResource::Disk(vmgs)
3416            | PetriVmgsResource::ReprovisionOnFailure(vmgs)
3417            | PetriVmgsResource::Reprovision(vmgs) => Some(vmgs),
3418            PetriVmgsResource::Ephemeral => None,
3419        }
3420    }
3421
3422    /// get the inner disk if one exists
3423    pub fn disk(&self) -> Option<&Disk> {
3424        self.vmgs().map(|vmgs| &vmgs.disk)
3425    }
3426
3427    /// get the encryption policy of the vmgs
3428    pub fn encryption_policy(&self) -> Option<GuestStateEncryptionPolicy> {
3429        self.vmgs().map(|vmgs| vmgs.encryption_policy)
3430    }
3431}
3432
3433/// Petri VM guest state lifetime
3434#[derive(Debug, Clone, Copy)]
3435pub enum PetriGuestStateLifetime {
3436    /// Use a differencing disk backed by a blank, tempory VMGS file
3437    /// or other artifact if one is provided
3438    Disk,
3439    /// Same as default, except reformat the backing disk if corrupted
3440    ReprovisionOnFailure,
3441    /// Same as default, except reformat the backing disk
3442    Reprovision,
3443    /// Store guest state in memory (no backing disk)
3444    Ephemeral,
3445}
3446
3447/// UEFI secure boot template
3448#[derive(Debug, Clone, Copy)]
3449pub enum SecureBootTemplate {
3450    /// The Microsoft Windows template.
3451    MicrosoftWindows,
3452    /// The Microsoft UEFI certificate authority template.
3453    MicrosoftUefiCertificateAuthority,
3454}
3455
3456/// Quirks to workaround certain bugs that only manifest when using a
3457/// particular VMM, and do not depend on which guest is running.
3458#[derive(Default, Debug, Clone)]
3459pub struct VmmQuirks {
3460    /// Automatically reset the VM if we did not recieve a boot event in the
3461    /// specified amount of time.
3462    pub flaky_boot: Option<Duration>,
3463}
3464
3465/// Creates a VM-safe name that respects platform limitations.
3466///
3467/// Hyper-V limits VM names to 100 characters. For names that exceed this limit,
3468/// this function truncates to 96 characters and appends a 4-character hash
3469/// to ensure uniqueness while staying within the limit.
3470fn make_vm_safe_name(name: &str) -> String {
3471    const MAX_VM_NAME_LENGTH: usize = 100;
3472    const HASH_LENGTH: usize = 4;
3473    const MAX_PREFIX_LENGTH: usize = MAX_VM_NAME_LENGTH - HASH_LENGTH;
3474
3475    if name.len() <= MAX_VM_NAME_LENGTH {
3476        name.to_owned()
3477    } else {
3478        // Create a hash of the full name for uniqueness
3479        let mut hasher = DefaultHasher::new();
3480        name.hash(&mut hasher);
3481        let hash = hasher.finish();
3482
3483        // Format hash as a 4-character hex string
3484        let hash_suffix = format!("{:04x}", hash & 0xFFFF);
3485
3486        // Truncate the name and append the hash
3487        let truncated = &name[..MAX_PREFIX_LENGTH];
3488        tracing::debug!(
3489            "VM name too long ({}), truncating '{}' to '{}{}'",
3490            name.len(),
3491            name,
3492            truncated,
3493            hash_suffix
3494        );
3495
3496        format!("{}{}", truncated, hash_suffix)
3497    }
3498}
3499
3500/// The reason that the VM halted
3501#[derive(Debug, Clone, Copy, Eq, PartialEq)]
3502pub enum PetriHaltReason {
3503    /// The vm powered off
3504    PowerOff,
3505    /// The vm reset
3506    Reset,
3507    /// The vm hibernated
3508    Hibernate,
3509    /// The vm triple faulted
3510    TripleFault,
3511    /// The vm halted for some other reason
3512    Other,
3513}
3514
3515impl PetriHaltReason {
3516    /// Construct a halt reason with detailed debug info
3517    pub fn with_detail(self, detail: String) -> PetriHaltReasonDetail {
3518        PetriHaltReasonDetail {
3519            reason: self,
3520            detail,
3521        }
3522    }
3523}
3524
3525/// The reason that the VM halted, with optional addition debug details
3526#[derive(Debug, Clone)]
3527pub struct PetriHaltReasonDetail {
3528    /// The reason for the halt
3529    pub reason: PetriHaltReason,
3530    /// More details about the halt
3531    pub detail: String,
3532}
3533
3534fn append_cmdline(cmd: &mut Option<String>, add_cmd: impl AsRef<str>) {
3535    if let Some(cmd) = cmd.as_mut() {
3536        cmd.push(' ');
3537        cmd.push_str(add_cmd.as_ref());
3538    } else {
3539        *cmd = Some(add_cmd.as_ref().to_string());
3540    }
3541}
3542
3543async fn save_inspect(
3544    name: &str,
3545    inspect: std::pin::Pin<Box<dyn Future<Output = anyhow::Result<inspect::Node>> + Send>>,
3546    log_source: &PetriLogSource,
3547) {
3548    tracing::info!("Collecting {name} inspect details.");
3549    let node = match inspect.await {
3550        Ok(n) => n,
3551        Err(e) => {
3552            tracing::error!(?e, "Failed to get {name}");
3553            return;
3554        }
3555    };
3556    if let Err(e) = log_source.write_attachment(
3557        &format!("timeout_inspect_{name}.log"),
3558        format!("{node:#}").as_bytes(),
3559    ) {
3560        tracing::error!(?e, "Failed to save {name} inspect log");
3561        return;
3562    }
3563    tracing::info!("{name} inspect task finished.");
3564}
3565
3566/// Wrapper for modification functions with stubbed out debug impl
3567pub struct ModifyFn<T>(pub Box<dyn FnOnce(T) -> T + Send>);
3568
3569impl<T> Debug for ModifyFn<T> {
3570    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3571        write!(f, "_")
3572    }
3573}
3574
3575/// Default VTL 2 settings used by petri
3576fn default_vtl2_settings() -> Vtl2Settings {
3577    Vtl2Settings {
3578        version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
3579        fixed: None,
3580        dynamic: Some(Default::default()),
3581        namespace_settings: Default::default(),
3582    }
3583}
3584
3585/// Virtual trust level
3586#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3587pub enum Vtl {
3588    /// VTL 0
3589    Vtl0 = 0,
3590    /// VTL 1
3591    Vtl1 = 1,
3592    /// VTL 2
3593    Vtl2 = 2,
3594}
3595
3596/// The VMBus storage device type.
3597#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3598pub enum VmbusStorageType {
3599    /// SCSI
3600    Scsi,
3601    /// NVMe
3602    Nvme,
3603    /// Virtio block device
3604    VirtioBlk,
3605}
3606
3607/// VM disk drive
3608#[derive(Debug, Clone)]
3609pub struct Drive {
3610    /// Backing disk
3611    pub disk: Option<Disk>,
3612    /// Whether this is a DVD
3613    pub is_dvd: bool,
3614}
3615
3616impl Drive {
3617    /// Create a new disk
3618    pub fn new(disk: Option<Disk>, is_dvd: bool) -> Self {
3619        Self { disk, is_dvd }
3620    }
3621}
3622
3623/// VMBus storage controller
3624#[derive(Debug, Clone)]
3625pub struct VmbusStorageController {
3626    /// The VTL to assign the storage controller to
3627    pub target_vtl: Vtl,
3628    /// The storage device type
3629    pub controller_type: VmbusStorageType,
3630    /// Drives (with any inserted disks) attached to this storage controller
3631    pub drives: HashMap<u32, Drive>,
3632}
3633
3634impl VmbusStorageController {
3635    /// Create a new storage controller
3636    pub fn new(target_vtl: Vtl, controller_type: VmbusStorageType) -> Self {
3637        Self {
3638            target_vtl,
3639            controller_type,
3640            drives: HashMap::new(),
3641        }
3642    }
3643
3644    /// Add a disk to the storage controller
3645    pub fn set_drive(
3646        &mut self,
3647        lun: Option<u32>,
3648        drive: Drive,
3649        allow_modify_existing: bool,
3650    ) -> u32 {
3651        let lun = lun.unwrap_or_else(|| {
3652            // find the first available lun
3653            let mut lun = None;
3654            for x in 0..u8::MAX as u32 {
3655                if !self.drives.contains_key(&x) {
3656                    lun = Some(x);
3657                    break;
3658                }
3659            }
3660            lun.expect("all locations on this controller are in use")
3661        });
3662
3663        if self.drives.insert(lun, drive).is_some() && !allow_modify_existing {
3664            panic!("a disk with lun {lun} already existed on this controller");
3665        }
3666
3667        lun
3668    }
3669}
3670
3671/// Returns the cache directory for lazy-fetched disk artifacts.
3672pub(crate) fn petri_disk_cache_dir() -> String {
3673    if let Ok(dir) = std::env::var("PETRI_CACHE_DIR") {
3674        return dir;
3675    }
3676
3677    #[cfg(target_os = "macos")]
3678    {
3679        if let Ok(home) = std::env::var("HOME") {
3680            return format!("{home}/Library/Caches/petri");
3681        }
3682    }
3683
3684    #[cfg(windows)]
3685    {
3686        if let Ok(local) = std::env::var("LOCALAPPDATA") {
3687            return format!("{local}\\petri\\cache");
3688        }
3689    }
3690
3691    // Linux / fallback: XDG
3692    if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
3693        return format!("{xdg}/petri");
3694    }
3695    if let Ok(home) = std::env::var("HOME") {
3696        return format!("{home}/.cache/petri");
3697    }
3698
3699    ".cache/petri".to_string()
3700}
3701
3702#[cfg(test)]
3703mod tests {
3704    use super::make_vm_safe_name;
3705    use crate::Drive;
3706    use crate::VmbusStorageController;
3707    use crate::VmbusStorageType;
3708    use crate::Vtl;
3709
3710    #[test]
3711    fn test_short_names_unchanged() {
3712        let short_name = "short_test_name";
3713        assert_eq!(make_vm_safe_name(short_name), short_name);
3714    }
3715
3716    #[test]
3717    fn test_exactly_100_chars_unchanged() {
3718        let name_100 = "a".repeat(100);
3719        assert_eq!(make_vm_safe_name(&name_100), name_100);
3720    }
3721
3722    #[test]
3723    fn test_long_name_truncated() {
3724        let long_name = "multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_openhcl_servicing";
3725        let result = make_vm_safe_name(long_name);
3726
3727        // Should be exactly 100 characters
3728        assert_eq!(result.len(), 100);
3729
3730        // Should start with the truncated prefix
3731        assert!(result.starts_with("multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_ope"));
3732
3733        // Should end with a 4-character hash
3734        let suffix = &result[96..];
3735        assert_eq!(suffix.len(), 4);
3736        // Should be valid hex
3737        assert!(u16::from_str_radix(suffix, 16).is_ok());
3738    }
3739
3740    #[test]
3741    fn test_deterministic_results() {
3742        let long_name = "very_long_test_name_that_exceeds_the_100_character_limit_and_should_be_truncated_consistently_every_time";
3743        let result1 = make_vm_safe_name(long_name);
3744        let result2 = make_vm_safe_name(long_name);
3745
3746        assert_eq!(result1, result2);
3747        assert_eq!(result1.len(), 100);
3748    }
3749
3750    #[test]
3751    fn test_different_names_different_hashes() {
3752        let name1 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_1";
3753        let name2 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_2";
3754
3755        let result1 = make_vm_safe_name(name1);
3756        let result2 = make_vm_safe_name(name2);
3757
3758        // Both should be 100 chars
3759        assert_eq!(result1.len(), 100);
3760        assert_eq!(result2.len(), 100);
3761
3762        // Should have different suffixes since the full names are different
3763        assert_ne!(result1, result2);
3764        assert_ne!(&result1[96..], &result2[96..]);
3765    }
3766
3767    #[test]
3768    fn test_vmbus_storage_controller() {
3769        let mut controller = VmbusStorageController::new(Vtl::Vtl0, VmbusStorageType::Scsi);
3770        assert_eq!(
3771            controller.set_drive(Some(1), Drive::new(None, false), false),
3772            1
3773        );
3774        assert!(controller.drives.contains_key(&1));
3775        assert_eq!(
3776            controller.set_drive(None, Drive::new(None, false), false),
3777            0
3778        );
3779        assert!(controller.drives.contains_key(&0));
3780        assert_eq!(
3781            controller.set_drive(None, Drive::new(None, false), false),
3782            2
3783        );
3784        assert!(controller.drives.contains_key(&2));
3785        assert_eq!(
3786            controller.set_drive(Some(0), Drive::new(None, false), true),
3787            0
3788        );
3789        assert!(controller.drives.contains_key(&0));
3790    }
3791}