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