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("").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    /// Set the hardware sealing policy for the VM's TPM.
1539    pub fn with_hardware_sealing_policy(mut self, policy: PetriHardwareSealingPolicy) -> Self {
1540        self.config
1541            .tpm
1542            .as_mut()
1543            .expect("hardware sealing policy requires a TPM")
1544            .hardware_sealing_policy = policy;
1545        self
1546    }
1547
1548    /// Add custom VTL 2 settings.
1549    // TODO: At some point we want to replace uses of this with nicer with_disk,
1550    // with_nic, etc. methods.
1551    pub fn with_custom_vtl2_settings(
1552        mut self,
1553        f: impl FnOnce(&mut Vtl2Settings) + 'static + Send + Sync,
1554    ) -> Self {
1555        f(self
1556            .config
1557            .firmware
1558            .vtl2_settings()
1559            .expect("Custom VTL 2 settings are only supported with OpenHCL"));
1560        self
1561    }
1562
1563    /// Add a storage controller to VTL2
1564    pub fn add_vtl2_storage_controller(self, controller: StorageController) -> Self {
1565        self.with_custom_vtl2_settings(move |v| {
1566            v.dynamic
1567                .as_mut()
1568                .unwrap()
1569                .storage_controllers
1570                .push(controller)
1571        })
1572    }
1573
1574    /// Add an additional SCSI controller to the VM.
1575    pub fn add_vmbus_storage_controller(
1576        mut self,
1577        id: &Guid,
1578        target_vtl: Vtl,
1579        controller_type: VmbusStorageType,
1580    ) -> Self {
1581        if self
1582            .config
1583            .vmbus_storage_controllers
1584            .insert(
1585                *id,
1586                VmbusStorageController::new(target_vtl, controller_type),
1587            )
1588            .is_some()
1589        {
1590            panic!("storage controller {id} already existed");
1591        }
1592        self
1593    }
1594
1595    /// Add a VMBus disk drive to the VM
1596    pub fn add_vmbus_drive(
1597        mut self,
1598        drive: Drive,
1599        controller_id: &Guid,
1600        controller_location: Option<u32>,
1601    ) -> Self {
1602        let controller = self
1603            .config
1604            .vmbus_storage_controllers
1605            .get_mut(controller_id)
1606            .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
1607
1608        _ = controller.set_drive(controller_location, drive, false);
1609
1610        self
1611    }
1612
1613    /// Add a VMBus disk drive to the VM
1614    pub fn add_ide_drive(
1615        mut self,
1616        drive: Drive,
1617        controller_number: u32,
1618        controller_location: u8,
1619    ) -> Self {
1620        self.config
1621            .firmware
1622            .ide_controllers_mut()
1623            .expect("Host IDE requires PCAT with no HCL")[controller_number as usize]
1624            [controller_location as usize] = Some(drive);
1625
1626        self
1627    }
1628
1629    /// Add a physical NVMe device to the VM
1630    pub fn add_physical_nvme_device(mut self, vsid: Guid, device: PhysicalNvmeDevice) -> Self {
1631        if self
1632            .config
1633            .physical_nvme_devices
1634            .insert(vsid, device)
1635            .is_some()
1636        {
1637            panic!("physical NVMe device {vsid} already existed");
1638        }
1639        self
1640    }
1641
1642    /// Get VM's guest OS flavor
1643    pub fn os_flavor(&self) -> OsFlavor {
1644        self.config.firmware.os_flavor()
1645    }
1646
1647    /// Get whether the VM will use OpenHCL
1648    pub fn is_openhcl(&self) -> bool {
1649        self.config.firmware.is_openhcl()
1650    }
1651
1652    /// Get the isolation type of the VM
1653    pub fn isolation(&self) -> Option<IsolationType> {
1654        self.config.firmware.isolation()
1655    }
1656
1657    /// Get the machine architecture
1658    pub fn arch(&self) -> MachineArch {
1659        self.config.arch
1660    }
1661
1662    /// Get the log source for creating additional log files.
1663    pub fn log_source(&self) -> &PetriLogSource {
1664        &self.resources.log_source
1665    }
1666
1667    /// Get the default OpenHCL servicing flags for this config
1668    pub fn default_servicing_flags(&self) -> OpenHclServicingFlags {
1669        T::default_servicing_flags()
1670    }
1671
1672    /// Get the backend-specific config builder
1673    pub fn modify_backend(
1674        mut self,
1675        f: impl FnOnce(T::VmmConfig) -> T::VmmConfig + 'static + Send,
1676    ) -> Self {
1677        if self.modify_vmm_config.is_some() {
1678            panic!("only one modify_backend allowed");
1679        }
1680        self.modify_vmm_config = Some(ModifyFn(Box::new(f)));
1681        self
1682    }
1683}
1684
1685impl<T: PetriVmmBackend> PetriVm<T> {
1686    /// Immediately tear down the VM.
1687    pub async fn teardown(self) -> anyhow::Result<()> {
1688        tracing::info!("Tearing down VM...");
1689        self.runtime.teardown().await
1690    }
1691
1692    /// Wait for the VM to halt, returning the reason for the halt.
1693    pub async fn wait_for_halt(&mut self) -> anyhow::Result<PetriHaltReasonDetail> {
1694        tracing::info!("Waiting for VM to halt...");
1695        let halt_reason = self.runtime.wait_for_halt(false).await?;
1696        tracing::info!("VM halted: {halt_reason:?}. Cancelling watchdogs...");
1697        futures::future::join_all(self.watchdog_tasks.drain(..).map(|t| t.cancel())).await;
1698        Ok(halt_reason)
1699    }
1700
1701    /// Wait for the VM to cleanly shutdown.
1702    pub async fn wait_for_clean_shutdown(&mut self) -> anyhow::Result<()> {
1703        let halt_reason = self.wait_for_halt().await?;
1704        if halt_reason.reason != PetriHaltReason::PowerOff {
1705            anyhow::bail!("Expected PowerOff, got {halt_reason:?}");
1706        }
1707        tracing::info!("VM was cleanly powered off and torn down.");
1708        Ok(())
1709    }
1710
1711    /// Wait for the VM to halt, returning the reason for the halt,
1712    /// and tear down the VM.
1713    pub async fn wait_for_teardown(mut self) -> anyhow::Result<PetriHaltReasonDetail> {
1714        let halt_reason = self.wait_for_halt().await?;
1715        self.teardown().await?;
1716        Ok(halt_reason)
1717    }
1718
1719    /// Wait for the VM to cleanly shutdown and tear down the VM.
1720    pub async fn wait_for_clean_teardown(mut self) -> anyhow::Result<()> {
1721        self.wait_for_clean_shutdown().await?;
1722        self.teardown().await
1723    }
1724
1725    /// Wait for the VM to reset. Does not wait for pipette.
1726    pub async fn wait_for_reset_no_agent(&mut self) -> anyhow::Result<()> {
1727        self.wait_for_reset_core().await?;
1728        self.wait_for_expected_boot_event().await?;
1729        Ok(())
1730    }
1731
1732    /// Wait for the VM to reset and pipette to connect.
1733    pub async fn wait_for_reset(&mut self) -> anyhow::Result<PipetteClient> {
1734        self.wait_for_reset_no_agent().await?;
1735        self.wait_for_agent().await
1736    }
1737
1738    async fn wait_for_reset_core(&mut self) -> anyhow::Result<()> {
1739        tracing::info!("Waiting for VM to reset...");
1740        let halt_reason = self.runtime.wait_for_halt(true).await?;
1741        if halt_reason.reason != PetriHaltReason::Reset {
1742            anyhow::bail!("Expected reset, got {halt_reason:?}");
1743        }
1744        tracing::info!("VM reset.");
1745        Ok(())
1746    }
1747
1748    /// Invoke Inspect on the running OpenHCL instance.
1749    ///
1750    /// IMPORTANT: As mentioned in the Guide, inspect output is *not* guaranteed
1751    /// to be stable. Use this to test that components in OpenHCL are working as
1752    /// you would expect. But, if you are adding a test simply to verify that
1753    /// the inspect output as some other tool depends on it, then that is
1754    /// incorrect.
1755    ///
1756    /// - `timeout` is enforced on the client side
1757    /// - `path` and `depth` are passed to the [`inspect::Inspect`] machinery.
1758    pub async fn inspect_openhcl(
1759        &self,
1760        path: impl Into<String>,
1761        depth: Option<usize>,
1762        timeout: Option<Duration>,
1763    ) -> anyhow::Result<inspect::Node> {
1764        self.openhcl_diag()?
1765            .inspect(path.into().as_str(), depth, timeout)
1766            .await
1767    }
1768
1769    /// Invoke Update (Inspect protocol) on the running OpenHCL instance.
1770    ///
1771    /// IMPORTANT: As mentioned in the Guide, inspect output is *not* guaranteed
1772    /// to be stable. Use this to test that components in OpenHCL are working as
1773    /// you would expect. But, if you are adding a test simply to verify that
1774    /// the inspect output as some other tool depends on it, then that is
1775    /// incorrect.
1776    ///
1777    /// - `path` and `value` are passed to the [`inspect::Inspect`] machinery.
1778    pub async fn inspect_update_openhcl(
1779        &self,
1780        path: impl Into<String>,
1781        value: impl Into<String>,
1782    ) -> anyhow::Result<inspect::Value> {
1783        self.openhcl_diag()?
1784            .inspect_update(path.into(), value.into())
1785            .await
1786    }
1787
1788    /// Test that we are able to inspect OpenHCL.
1789    pub async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()> {
1790        self.inspect_openhcl("", None, None).await.map(|_| ())
1791    }
1792
1793    /// Invoke Inspect on the running VMM process itself (e.g. OpenVMM),
1794    /// returning the inspect tree rooted at `path` (pass `""` for the whole
1795    /// tree).
1796    ///
1797    /// Only backends that expose an inspect interface (currently OpenVMM)
1798    /// support this; other backends return an error.
1799    ///
1800    /// IMPORTANT: As mentioned in the Guide, inspect output is *not* guaranteed
1801    /// to be stable. Use this to verify that components are working as you
1802    /// expect, not to assert on output that some other tool depends on.
1803    pub async fn inspect_vmm(&self, path: &str) -> anyhow::Result<inspect::Node> {
1804        use anyhow::Context;
1805
1806        let inspector = self
1807            .runtime
1808            .inspector()
1809            .context("this VMM backend does not support inspect")?;
1810        inspector.inspect(path).await
1811    }
1812
1813    /// Wait for VTL 2 to report that it is ready to respond to commands.
1814    /// Will fail if the VM is not running OpenHCL.
1815    ///
1816    /// This should only be necessary if you're doing something manual. All
1817    /// Petri-provided methods will wait for VTL 2 to be ready automatically.
1818    pub async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()> {
1819        self.openhcl_diag()?.wait_for_vtl2().await
1820    }
1821
1822    /// Get the kmsg stream from OpenHCL.
1823    pub async fn kmsg(&self) -> anyhow::Result<diag_client::kmsg_stream::KmsgStream> {
1824        self.openhcl_diag()?.kmsg().await
1825    }
1826
1827    /// Gets a live core dump of the OpenHCL process specified by 'name' and
1828    /// writes it to 'path'
1829    pub async fn openhcl_core_dump(&self, name: &str, path: &Path) -> anyhow::Result<()> {
1830        self.openhcl_diag()?.core_dump(name, path).await
1831    }
1832
1833    /// Crashes the specified openhcl process
1834    pub async fn openhcl_crash(&self, name: &str) -> anyhow::Result<()> {
1835        self.openhcl_diag()?.crash(name).await
1836    }
1837
1838    /// Wait for a connection from a pipette agent running in the guest.
1839    /// Useful if you've rebooted the vm or are otherwise expecting a fresh connection.
1840    async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient> {
1841        // As a workaround for #2470 (where the guest crashes when the pipette
1842        // connection timeout expires due to a vmbus bug), wait for the shutdown
1843        // IC to come online first so that we probably won't time out when
1844        // connecting to the agent.
1845        // TODO: remove this once the bug is fixed, since it shouldn't be
1846        // necessary and a guest could in theory support pipette and not the IC
1847        //
1848        // This is a no-op when the shutdown IC is not configured (e.g.,
1849        // no VMBus or minimal mode).
1850        self.runtime.wait_for_enlightened_shutdown_ready().await?;
1851        self.runtime.wait_for_agent(false).await
1852    }
1853
1854    /// Wait for a connection from a pipette agent running in VTL 2.
1855    /// Useful if you've reset VTL 2 or are otherwise expecting a fresh connection.
1856    /// Will fail if the VM is not running OpenHCL.
1857    pub async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient> {
1858        // VTL 2's pipette doesn't auto launch, only launch it on demand
1859        self.launch_vtl2_pipette().await?;
1860        self.runtime.wait_for_agent(true).await
1861    }
1862
1863    /// Waits for an event emitted by the firmware about its boot status, and
1864    /// verifies that it is the expected success value.
1865    ///
1866    /// * Linux Direct guests do not emit a boot event, so this method immediately returns Ok.
1867    /// * PCAT guests may not emit an event depending on the PCAT version, this
1868    ///   method is best effort for them.
1869    async fn wait_for_expected_boot_event(&mut self) -> anyhow::Result<()> {
1870        if let Some(expected_event) = self.expected_boot_event {
1871            let event = self.wait_for_boot_event().await?;
1872
1873            anyhow::ensure!(
1874                event == expected_event,
1875                "Did not receive expected boot event"
1876            );
1877        } else {
1878            tracing::warn!("Boot event not emitted for configured firmware or manually ignored.");
1879        }
1880
1881        Ok(())
1882    }
1883
1884    /// Waits for an event emitted by the firmware about its boot status, and
1885    /// returns that status.
1886    async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
1887        tracing::info!("Waiting for boot event...");
1888        let boot_event = loop {
1889            match CancelContext::new()
1890                .with_timeout(self.vmm_quirks.flaky_boot.unwrap_or(Duration::MAX))
1891                .until_cancelled(self.runtime.wait_for_boot_event())
1892                .await
1893            {
1894                Ok(res) => break res?,
1895                Err(_) => {
1896                    tracing::error!("Did not get boot event in required time, resetting...");
1897                    if let Some(inspector) = self.runtime.inspector() {
1898                        save_inspect(
1899                            "vmm",
1900                            Box::pin(async move { inspector.inspect("").await }),
1901                            &self.resources.log_source,
1902                        )
1903                        .await;
1904                    }
1905
1906                    self.runtime.reset().await?;
1907                    continue;
1908                }
1909            }
1910        };
1911        tracing::info!("Got boot event: {boot_event:?}");
1912        Ok(boot_event)
1913    }
1914
1915    /// Wait for the Hyper-V shutdown IC to be ready and use it to instruct
1916    /// the guest to shutdown.
1917    pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
1918        tracing::info!("Waiting for enlightened shutdown to be ready");
1919        self.runtime.wait_for_enlightened_shutdown_ready().await?;
1920
1921        // all guests used in testing have been observed to intermittently
1922        // drop shutdown requests if they are sent too soon after the shutdown
1923        // ic comes online. give them a little extra time.
1924        // TODO: use a different method of determining whether the VM has booted
1925        // or debug and fix the shutdown IC.
1926        let mut wait_time = Duration::from_secs(10);
1927
1928        // some guests need even more time
1929        if let Some(duration) = self.guest_quirks.hyperv_shutdown_ic_sleep {
1930            wait_time += duration;
1931        }
1932
1933        tracing::info!(
1934            "Shutdown IC reported ready, waiting for an extra {}s",
1935            wait_time.as_secs()
1936        );
1937        PolledTimer::new(&self.resources.driver)
1938            .sleep(wait_time)
1939            .await;
1940
1941        tracing::info!("Sending enlightened shutdown command");
1942        self.runtime.send_enlightened_shutdown(kind).await
1943    }
1944
1945    /// Instruct the OpenHCL to restart the VTL2 paravisor. Will fail if the VM
1946    /// is not running OpenHCL. Will also fail if the VM is not running.
1947    pub async fn restart_openhcl(
1948        &mut self,
1949        new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1950        flags: OpenHclServicingFlags,
1951    ) -> anyhow::Result<()> {
1952        self.runtime
1953            .restart_openhcl(&new_openhcl.erase(), flags)
1954            .await
1955    }
1956
1957    /// Update the command line parameter of the running VM that will apply on next boot.
1958    /// Will fail if the VM is not using IGVM load mode.
1959    pub async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()> {
1960        self.runtime.update_command_line(command_line).await
1961    }
1962
1963    /// Hot-add a PCIe device to a named port at runtime.
1964    pub async fn add_pcie_device(
1965        &mut self,
1966        port_name: String,
1967        resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
1968    ) -> anyhow::Result<()> {
1969        self.runtime.add_pcie_device(port_name, resource).await
1970    }
1971
1972    /// Hot-remove a PCIe device from a named port at runtime.
1973    pub async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
1974        self.runtime.remove_pcie_device(port_name).await
1975    }
1976
1977    /// Instruct the OpenHCL to save the state of the VTL2 paravisor. Will fail if the VM
1978    /// is not running OpenHCL. Will also fail if the VM is not running or if this is called twice in succession
1979    pub async fn save_openhcl(
1980        &mut self,
1981        new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1982        flags: OpenHclServicingFlags,
1983    ) -> anyhow::Result<()> {
1984        self.runtime.save_openhcl(&new_openhcl.erase(), flags).await
1985    }
1986
1987    /// Instruct the OpenHCL to restore the state of the VTL2 paravisor. Will fail if the VM
1988    /// is not running OpenHCL. Will also fail if the VM is running or if this is called without prior save
1989    pub async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
1990        self.runtime.restore_openhcl().await
1991    }
1992
1993    /// Get VM's guest OS flavor
1994    pub fn arch(&self) -> MachineArch {
1995        self.arch
1996    }
1997
1998    /// Get the inner runtime backend to make backend-specific calls
1999    pub fn backend(&mut self) -> &mut T::VmRuntime {
2000        &mut self.runtime
2001    }
2002
2003    async fn launch_vtl2_pipette(&self) -> anyhow::Result<()> {
2004        tracing::debug!("Launching VTL 2 pipette...");
2005
2006        // Start pipette through DiagClient
2007        let res = self
2008            .openhcl_diag()?
2009            .run_vtl2_command("sh", &["-c", "mkdir /cidata && mount LABEL=cidata /cidata"])
2010            .await?;
2011
2012        if !res.exit_status.success() {
2013            anyhow::bail!("Failed to mount VTL 2 pipette drive: {:?}", res);
2014        }
2015
2016        let res = self
2017            .openhcl_diag()?
2018            .run_detached_vtl2_command("sh", &["-c", "/cidata/pipette 2>&1 | logger &"])
2019            .await?;
2020
2021        if !res.success() {
2022            anyhow::bail!("Failed to spawn VTL 2 pipette: {:?}", res);
2023        }
2024
2025        Ok(())
2026    }
2027
2028    fn openhcl_diag(&self) -> anyhow::Result<&OpenHclDiagHandler> {
2029        if let Some(ohd) = self.openhcl_diag_handler.as_ref() {
2030            Ok(ohd)
2031        } else {
2032            anyhow::bail!("VM is not configured with OpenHCL")
2033        }
2034    }
2035
2036    /// Get the path to the VM's guest state file
2037    pub async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2038        self.runtime.get_guest_state_file().await
2039    }
2040
2041    /// Modify OpenHCL VTL2 settings.
2042    pub async fn modify_vtl2_settings(
2043        &mut self,
2044        f: impl FnOnce(&mut Vtl2Settings),
2045    ) -> anyhow::Result<()> {
2046        if self.openhcl_diag_handler.is_none() {
2047            panic!("Custom VTL 2 settings are only supported with OpenHCL");
2048        }
2049        f(self
2050            .config
2051            .vtl2_settings
2052            .get_or_insert_with(default_vtl2_settings));
2053        self.runtime
2054            .set_vtl2_settings(self.config.vtl2_settings.as_ref().unwrap())
2055            .await
2056    }
2057
2058    /// Get the list of storage controllers added to this VM
2059    pub fn get_vmbus_storage_controllers(&self) -> &HashMap<Guid, VmbusStorageController> {
2060        &self.config.vmbus_storage_controllers
2061    }
2062
2063    /// Add or modify a VMBus disk drive
2064    pub async fn set_vmbus_drive(
2065        &mut self,
2066        drive: Drive,
2067        controller_id: &Guid,
2068        controller_location: Option<u32>,
2069    ) -> anyhow::Result<()> {
2070        let controller = self
2071            .config
2072            .vmbus_storage_controllers
2073            .get_mut(controller_id)
2074            .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
2075
2076        let controller_location = controller.set_drive(controller_location, drive, true);
2077        let disk = controller.drives.get(&controller_location).unwrap();
2078
2079        self.runtime
2080            .set_vmbus_drive(disk, controller_id, controller_location)
2081            .await?;
2082
2083        Ok(())
2084    }
2085}
2086
2087/// A running VM that tests can interact with.
2088#[async_trait]
2089pub trait PetriVmRuntime: Send + Sync + 'static {
2090    /// Interface for inspecting the VM
2091    type VmInspector: PetriVmInspector;
2092    /// Interface for accessing the framebuffer
2093    type VmFramebufferAccess: PetriVmFramebufferAccess;
2094
2095    /// Cleanly tear down the VM immediately.
2096    async fn teardown(self) -> anyhow::Result<()>;
2097    /// Wait for the VM to halt, returning the reason for the halt. The VM
2098    /// should automatically restart the VM on reset if `allow_reset` is true.
2099    async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReasonDetail>;
2100    /// Wait for a connection from a pipette agent
2101    async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>;
2102    /// Get an OpenHCL diagnostics handler for the VM
2103    fn openhcl_diag(&self) -> Option<OpenHclDiagHandler>;
2104    /// Waits for an event emitted by the firmware about its boot status, and
2105    /// returns that status.
2106    async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent>;
2107    /// Waits for the Hyper-V shutdown IC to be ready
2108    // TODO: return a receiver that will be closed when it is no longer ready.
2109    async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()>;
2110    /// Instruct the guest to shutdown via the Hyper-V shutdown IC.
2111    async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
2112    /// Instruct the OpenHCL to restart the VTL2 paravisor. Will fail if the VM
2113    /// is not running OpenHCL. Will also fail if the VM is not running.
2114    async fn restart_openhcl(
2115        &mut self,
2116        new_openhcl: &ResolvedArtifact,
2117        flags: OpenHclServicingFlags,
2118    ) -> anyhow::Result<()>;
2119    /// Instruct the OpenHCL to save the state of the VTL2 paravisor. Will fail if the VM
2120    /// is not running OpenHCL. Will also fail if the VM is not running or if this is called twice in succession
2121    /// without a call to `restore_openhcl`.
2122    async fn save_openhcl(
2123        &mut self,
2124        new_openhcl: &ResolvedArtifact,
2125        flags: OpenHclServicingFlags,
2126    ) -> anyhow::Result<()>;
2127    /// Instruct the OpenHCL to restore the state of the VTL2 paravisor. Will fail if the VM
2128    /// is not running OpenHCL. Will also fail if the VM is running or if this is called without prior save.
2129    async fn restore_openhcl(&mut self) -> anyhow::Result<()>;
2130    /// Update the command line parameter of the running VM that will apply on next boot.
2131    /// Will fail if the VM is not using IGVM load mode.
2132    async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()>;
2133    /// If the backend supports it, get an inspect interface
2134    fn inspector(&self) -> Option<Self::VmInspector> {
2135        None
2136    }
2137    /// If the backend supports it, take the screenshot interface
2138    /// (subsequent calls may return None).
2139    fn take_framebuffer_access(&mut self) -> Option<Self::VmFramebufferAccess> {
2140        None
2141    }
2142    /// Issue a hard reset to the VM
2143    async fn reset(&mut self) -> anyhow::Result<()>;
2144    /// Get the path to the VM's guest state file
2145    async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2146        Ok(None)
2147    }
2148    /// Set the OpenHCL VTL2 settings
2149    async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()>;
2150    /// Add or modify a VMBus disk drive
2151    async fn set_vmbus_drive(
2152        &mut self,
2153        disk: &Drive,
2154        controller_id: &Guid,
2155        controller_location: u32,
2156    ) -> anyhow::Result<()>;
2157    /// Hot-add a PCIe device to a named port at runtime.
2158    async fn add_pcie_device(
2159        &mut self,
2160        port_name: String,
2161        resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
2162    ) -> anyhow::Result<()> {
2163        let _ = (port_name, resource);
2164        anyhow::bail!("PCIe hotplug not supported by this backend")
2165    }
2166    /// Hot-remove a PCIe device from a named port at runtime.
2167    async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
2168        let _ = port_name;
2169        anyhow::bail!("PCIe hotplug not supported by this backend")
2170    }
2171}
2172
2173/// Interface for getting information about the state of the VM
2174#[async_trait]
2175pub trait PetriVmInspector: Send + Sync + 'static {
2176    /// Get information about the state of the VM at the given inspect `path`.
2177    /// Pass `""` to inspect the entire tree.
2178    async fn inspect(&self, path: &str) -> anyhow::Result<inspect::Node>;
2179}
2180
2181/// Use this for the associated type if not supported
2182pub struct NoPetriVmInspector;
2183#[async_trait]
2184impl PetriVmInspector for NoPetriVmInspector {
2185    async fn inspect(&self, _path: &str) -> anyhow::Result<inspect::Node> {
2186        unreachable!()
2187    }
2188}
2189
2190/// Raw VM screenshot
2191pub struct VmScreenshotMeta {
2192    /// color encoding used by the image
2193    pub color: image::ExtendedColorType,
2194    /// x dimension
2195    pub width: u16,
2196    /// y dimension
2197    pub height: u16,
2198}
2199
2200/// Interface for getting screenshots of the VM
2201#[async_trait]
2202pub trait PetriVmFramebufferAccess: Send + 'static {
2203    /// Populates the provided buffer with a screenshot of the VM,
2204    /// returning the dimensions and color type.
2205    async fn screenshot(&mut self, image: &mut Vec<u8>)
2206    -> anyhow::Result<Option<VmScreenshotMeta>>;
2207}
2208
2209/// Use this for the associated type if not supported
2210pub struct NoPetriVmFramebufferAccess;
2211#[async_trait]
2212impl PetriVmFramebufferAccess for NoPetriVmFramebufferAccess {
2213    async fn screenshot(
2214        &mut self,
2215        _image: &mut Vec<u8>,
2216    ) -> anyhow::Result<Option<VmScreenshotMeta>> {
2217        unreachable!()
2218    }
2219}
2220
2221/// Common processor topology information for the VM.
2222#[derive(Debug)]
2223pub struct ProcessorTopology {
2224    /// The number of virtual processors.
2225    pub vp_count: u32,
2226    /// Whether SMT (hyperthreading) is enabled.
2227    pub enable_smt: Option<bool>,
2228    /// The number of virtual processors per socket.
2229    pub vps_per_socket: Option<u32>,
2230    /// The APIC configuration (x86-64 only).
2231    pub apic_mode: Option<ApicMode>,
2232}
2233
2234impl Default for ProcessorTopology {
2235    fn default() -> Self {
2236        Self {
2237            vp_count: 2,
2238            enable_smt: None,
2239            vps_per_socket: None,
2240            apic_mode: None,
2241        }
2242    }
2243}
2244
2245impl ProcessorTopology {
2246    /// A large number of VPs
2247    pub fn heavy() -> Self {
2248        Self {
2249            vp_count: 16,
2250            vps_per_socket: Some(8),
2251            ..Default::default()
2252        }
2253    }
2254
2255    /// A very large number of VPs
2256    pub fn very_heavy() -> Self {
2257        Self {
2258            vp_count: 32,
2259            vps_per_socket: Some(16),
2260            ..Default::default()
2261        }
2262    }
2263}
2264
2265/// The APIC mode for the VM.
2266#[derive(Debug, Clone, Copy)]
2267pub enum ApicMode {
2268    /// xAPIC mode only.
2269    Xapic,
2270    /// x2APIC mode supported but not enabled at boot.
2271    X2apicSupported,
2272    /// x2APIC mode enabled at boot.
2273    X2apicEnabled,
2274}
2275
2276/// Common memory configuration information for the VM.
2277#[derive(Debug)]
2278pub struct MemoryConfig {
2279    /// Specifies the amount of memory, in bytes, to assign to the
2280    /// virtual machine.
2281    pub startup_bytes: u64,
2282    /// Specifies the minimum and maximum amount of dynamic memory, in bytes.
2283    ///
2284    /// Dynamic memory will be disabled if this is `None`.
2285    pub dynamic_memory_range: Option<(u64, u64)>,
2286    /// Per-NUMA-node memory sizes. When set, RAM is distributed across
2287    /// vNUMA nodes instead of assigning all RAM to node 0.
2288    pub numa_mem_sizes: Option<Vec<u64>>,
2289}
2290
2291impl Default for MemoryConfig {
2292    fn default() -> Self {
2293        Self {
2294            startup_bytes: 4 * 1024 * 1024 * 1024, // 4 GiB
2295            dynamic_memory_range: None,
2296            numa_mem_sizes: None,
2297        }
2298    }
2299}
2300
2301/// UEFI firmware configuration
2302#[derive(Debug)]
2303pub struct UefiConfig {
2304    /// Enable secure boot
2305    pub secure_boot_enabled: bool,
2306    /// Secure boot template
2307    pub secure_boot_template: Option<SecureBootTemplate>,
2308    /// Disable the UEFI frontpage which will cause the VM to shutdown instead when unable to boot.
2309    pub disable_frontpage: bool,
2310    /// Always attempt a default boot
2311    pub default_boot_always_attempt: bool,
2312    /// Enable vPCI boot (for NVMe)
2313    pub enable_vpci_boot: bool,
2314    /// Force UEFI to bounce-buffer all DMA traffic
2315    pub force_dma_bounce: bool,
2316    /// EFI diagnostics log level filter
2317    pub efi_diagnostics_log_level: EfiDiagnosticsLogLevel,
2318    /// Per-period rate-limit override for EFI diagnostics emission.
2319    /// See [`PetriVmBuilder::with_efi_diagnostics_rate_limit()`] for more information.
2320    pub efi_diagnostics_rate_limit: Option<u32>,
2321}
2322
2323impl Default for UefiConfig {
2324    fn default() -> Self {
2325        Self {
2326            secure_boot_enabled: false,
2327            secure_boot_template: None,
2328            disable_frontpage: true,
2329            default_boot_always_attempt: false,
2330            enable_vpci_boot: false,
2331            force_dma_bounce: false,
2332            efi_diagnostics_log_level: EfiDiagnosticsLogLevel::Default,
2333            efi_diagnostics_rate_limit: None,
2334        }
2335    }
2336}
2337
2338/// EFI diagnostics log level filter.
2339///
2340/// Controls which UEFI diagnostics log entries are forwarded to the host
2341/// tracing infrastructure (and thus visible via kmsg / test output).
2342#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2343pub enum EfiDiagnosticsLogLevel {
2344    /// Default log level (ERROR and WARN only).
2345    #[default]
2346    Default,
2347    /// Include INFO logs (ERROR, WARN, and INFO).
2348    Info,
2349    /// All log levels.
2350    Full,
2351}
2352
2353/// Control the logging configuration of OpenVMM/OpenHCL.
2354#[derive(Debug, Clone)]
2355pub enum OpenvmmLogConfig {
2356    /// Use the default log levels used by petri tests. This will forward
2357    /// `OPENVMM_LOG` and `OPENVMM_SHOW_SPANS` from the environment if they are
2358    /// set, otherwise it will use `debug` and `true` respectively
2359    TestDefault,
2360    /// Use the built-in default log levels of OpenHCL/OpenVMM (e.g. don't pass
2361    /// OPENVMM_LOG or OPENVMM_SHOW_SPANS)
2362    BuiltInDefault,
2363    /// Use the provided custom log levels, specified as key/value pairs. At this time,
2364    /// simply uses the already-defined environment variables (e.g.
2365    /// `OPENVMM_LOG=info,disk_nvme=debug OPENVMM_SHOW_SPANS=true`)
2366    ///
2367    /// See the Guide and source code for configuring these logs.
2368    /// - For the host VMM: see `enable_tracing` in `tracing_init.rs` for details on
2369    ///   the accepted keys and values.
2370    /// - For OpenHCL, see `init_tracing_backend` in `openhcl/src/logging/mod.rs` for details on
2371    ///   the accepted keys and values.
2372    Custom(BTreeMap<String, String>),
2373}
2374
2375/// OpenHCL configuration
2376#[derive(Debug)]
2377pub struct OpenHclConfig {
2378    /// Whether to enable VMBus redirection
2379    pub vmbus_redirect: bool,
2380    /// Test-specified command-line parameters to append to the petri generated
2381    /// command line and pass to OpenHCL. VM backends should use
2382    /// [`OpenHclConfig::command_line()`] rather than reading this directly.
2383    pub custom_command_line: Option<String>,
2384    /// Command line parameters that control OpenHCL logging behavior. Separate
2385    /// from `command_line` so that petri can decide to use default log
2386    /// levels.
2387    pub log_levels: OpenvmmLogConfig,
2388    /// How to place VTL2 in address space. If `None`, the backend VMM
2389    /// will decide on default behavior.
2390    pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
2391    /// VTL2 settings
2392    pub vtl2_settings: Option<Vtl2Settings>,
2393}
2394
2395impl OpenHclConfig {
2396    /// Returns the command line to pass to OpenHCL based on these parameters. Aggregates
2397    /// the command line and log levels.
2398    pub fn command_line(&self) -> String {
2399        let mut cmdline = self.custom_command_line.clone();
2400
2401        // Enable MANA keep-alive by default for all tests
2402        append_cmdline(&mut cmdline, "OPENHCL_MANA_KEEP_ALIVE=host,privatepool");
2403
2404        match &self.log_levels {
2405            OpenvmmLogConfig::TestDefault => {
2406                let default_log_levels = {
2407                    // Forward OPENVMM_LOG and OPENVMM_SHOW_SPANS to OpenHCL if they're set.
2408                    let openhcl_tracing = if let Ok(x) =
2409                        std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG"))
2410                    {
2411                        format!("OPENVMM_LOG={x}")
2412                    } else {
2413                        "OPENVMM_LOG=debug".to_owned()
2414                    };
2415                    let openhcl_show_spans = if let Ok(x) = std::env::var("OPENVMM_SHOW_SPANS") {
2416                        format!("OPENVMM_SHOW_SPANS={x}")
2417                    } else {
2418                        "OPENVMM_SHOW_SPANS=true".to_owned()
2419                    };
2420                    format!("{openhcl_tracing} {openhcl_show_spans}")
2421                };
2422                append_cmdline(&mut cmdline, &default_log_levels);
2423            }
2424            OpenvmmLogConfig::BuiltInDefault => {
2425                // do nothing, use whatever the built-in default is
2426            }
2427            OpenvmmLogConfig::Custom(levels) => {
2428                levels.iter().for_each(|(key, value)| {
2429                    append_cmdline(&mut cmdline, format!("{key}={value}"));
2430                });
2431            }
2432        }
2433
2434        cmdline.unwrap_or_default()
2435    }
2436}
2437
2438impl Default for OpenHclConfig {
2439    fn default() -> Self {
2440        Self {
2441            vmbus_redirect: false,
2442            custom_command_line: None,
2443            log_levels: OpenvmmLogConfig::TestDefault,
2444            vtl2_base_address_type: None,
2445            vtl2_settings: None,
2446        }
2447    }
2448}
2449
2450/// TPM configuration
2451#[derive(Debug)]
2452pub struct TpmConfig {
2453    /// Use ephemeral TPM state (do not persist to VMGS)
2454    pub no_persistent_secrets: bool,
2455    /// Hardware sealing policy for sealed secrets
2456    pub hardware_sealing_policy: PetriHardwareSealingPolicy,
2457}
2458
2459impl Default for TpmConfig {
2460    fn default() -> Self {
2461        Self {
2462            no_persistent_secrets: true,
2463            hardware_sealing_policy: PetriHardwareSealingPolicy::Default,
2464        }
2465    }
2466}
2467
2468/// Hardware sealing policy used by the test infrastructure.
2469///
2470/// Maps to Hyper-V `Set-GuestStateEncryptionPolicy` values and
2471/// underhill's `HardwareSealingPolicy`.
2472#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2473pub enum PetriHardwareSealingPolicy {
2474    /// No explicit policy — the backend picks its default.
2475    #[default]
2476    Default,
2477    /// Derive the hardware sealing key from measurement hash.
2478    HashPolicy,
2479    /// Derive the hardware sealing key from signer information.
2480    SignerPolicy,
2481}
2482
2483/// Firmware to load into the test VM.
2484// TODO: remove the guests from the firmware enum so that we don't pass them
2485// to the VMM backend after we have already used them generically.
2486#[derive(Debug)]
2487pub enum Firmware {
2488    /// Boot Linux directly, without any firmware.
2489    LinuxDirect {
2490        /// The kernel to boot.
2491        kernel: ResolvedArtifact,
2492        /// The initrd to use.
2493        initrd: ResolvedArtifact,
2494    },
2495    /// Boot Linux directly, without any firmware, with OpenHCL in VTL2.
2496    OpenhclLinuxDirect {
2497        /// The path to the IGVM file to use.
2498        igvm_path: ResolvedArtifact,
2499        /// OpenHCL configuration
2500        openhcl_config: OpenHclConfig,
2501    },
2502    /// Boot a PCAT-based VM.
2503    Pcat {
2504        /// The guest OS the VM will boot into.
2505        guest: PcatGuest,
2506        /// The firmware to use.
2507        bios_firmware: ResolvedOptionalArtifact,
2508        /// The SVGA firmware to use.
2509        svga_firmware: ResolvedOptionalArtifact,
2510        /// IDE controllers and associated disks
2511        ide_controllers: [[Option<Drive>; 2]; 2],
2512    },
2513    /// Boot a PCAT-based VM with OpenHCL in VTL2.
2514    OpenhclPcat {
2515        /// The guest OS the VM will boot into.
2516        guest: PcatGuest,
2517        /// The path to the IGVM file to use.
2518        igvm_path: ResolvedArtifact,
2519        /// The firmware to use.
2520        bios_firmware: ResolvedOptionalArtifact,
2521        /// The SVGA firmware to use.
2522        svga_firmware: ResolvedOptionalArtifact,
2523        /// OpenHCL configuration
2524        openhcl_config: OpenHclConfig,
2525    },
2526    /// Boot a UEFI-based VM.
2527    Uefi {
2528        /// The guest OS the VM will boot into.
2529        guest: UefiGuest,
2530        /// The firmware to use.
2531        uefi_firmware: ResolvedArtifact,
2532        /// UEFI configuration
2533        uefi_config: UefiConfig,
2534    },
2535    /// Boot a UEFI-based VM with OpenHCL in VTL2.
2536    OpenhclUefi {
2537        /// The guest OS the VM will boot into.
2538        guest: UefiGuest,
2539        /// The isolation type of the VM.
2540        isolation: Option<IsolationType>,
2541        /// The path to the IGVM file to use.
2542        igvm_path: ResolvedArtifact,
2543        /// UEFI configuration
2544        uefi_config: UefiConfig,
2545        /// OpenHCL configuration
2546        openhcl_config: OpenHclConfig,
2547    },
2548}
2549
2550/// The boot device type.
2551#[derive(Debug, Copy, Clone, PartialEq, Eq)]
2552pub enum BootDeviceType {
2553    /// Don't initialize a boot device.
2554    None,
2555    /// Boot from IDE.
2556    Ide,
2557    /// Boot from IDE via SCSI to VTL2.
2558    IdeViaScsi,
2559    /// Boot from IDE via NVME to VTL2.
2560    IdeViaNvme,
2561    /// Boot from SCSI.
2562    Scsi,
2563    /// Boot from SCSI via SCSI to VTL2.
2564    ScsiViaScsi,
2565    /// Boot from SCSI via NVME to VTL2.
2566    ScsiViaNvme,
2567    /// Boot from NVMe.
2568    Nvme,
2569    /// Boot from NVMe via SCSI to VTL2.
2570    NvmeViaScsi,
2571    /// Boot from NVMe via NVMe to VTL2.
2572    NvmeViaNvme,
2573    /// Boot from NVMe attached to a PCIe root port.
2574    PcieNvme,
2575}
2576
2577impl BootDeviceType {
2578    fn requires_vtl2(&self) -> bool {
2579        match self {
2580            BootDeviceType::None
2581            | BootDeviceType::Ide
2582            | BootDeviceType::Scsi
2583            | BootDeviceType::Nvme
2584            | BootDeviceType::PcieNvme => false,
2585            BootDeviceType::IdeViaScsi
2586            | BootDeviceType::IdeViaNvme
2587            | BootDeviceType::ScsiViaScsi
2588            | BootDeviceType::ScsiViaNvme
2589            | BootDeviceType::NvmeViaScsi
2590            | BootDeviceType::NvmeViaNvme => true,
2591        }
2592    }
2593
2594    fn requires_vpci_boot(&self) -> bool {
2595        matches!(
2596            self,
2597            BootDeviceType::Nvme | BootDeviceType::NvmeViaScsi | BootDeviceType::NvmeViaNvme
2598        )
2599    }
2600
2601    fn requires_vmbus(&self) -> bool {
2602        match self {
2603            BootDeviceType::None | BootDeviceType::Ide | BootDeviceType::PcieNvme => false,
2604            BootDeviceType::IdeViaScsi
2605            | BootDeviceType::IdeViaNvme
2606            | BootDeviceType::Scsi
2607            | BootDeviceType::ScsiViaScsi
2608            | BootDeviceType::ScsiViaNvme
2609            | BootDeviceType::Nvme
2610            | BootDeviceType::NvmeViaScsi
2611            | BootDeviceType::NvmeViaNvme => true,
2612        }
2613    }
2614}
2615
2616impl Firmware {
2617    /// Constructs a standard [`Firmware::LinuxDirect`] configuration.
2618    pub fn linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2619        use petri_artifacts_vmm_test::artifacts::loadable::*;
2620        match arch {
2621            MachineArch::X86_64 => Firmware::LinuxDirect {
2622                kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_X64).erase(),
2623                initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
2624            },
2625            MachineArch::Aarch64 => Firmware::LinuxDirect {
2626                kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_AARCH64).erase(),
2627                initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_AARCH64).erase(),
2628            },
2629        }
2630    }
2631
2632    /// Constructs a [`Firmware::LinuxDirect`] configuration that uses a
2633    /// compressed bzImage kernel instead of an uncompressed ELF.
2634    ///
2635    /// This is x86_64-only, as bzImage is an x86-specific format.
2636    pub fn linux_direct_bzimage(resolver: &ArtifactResolver<'_>) -> Self {
2637        use petri_artifacts_vmm_test::artifacts::loadable::*;
2638        Firmware::LinuxDirect {
2639            kernel: resolver.require(LINUX_DIRECT_TEST_BZIMAGE_X64).erase(),
2640            initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
2641        }
2642    }
2643
2644    /// Constructs a standard [`Firmware::OpenhclLinuxDirect`] configuration.
2645    pub fn openhcl_linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2646        use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2647        match arch {
2648            MachineArch::X86_64 => Firmware::OpenhclLinuxDirect {
2649                igvm_path: resolver.require(LATEST_LINUX_DIRECT_TEST_X64).erase(),
2650                openhcl_config: Default::default(),
2651            },
2652            MachineArch::Aarch64 => todo!("Linux direct not yet supported on aarch64"),
2653        }
2654    }
2655
2656    /// Constructs a standard [`Firmware::Pcat`] configuration.
2657    pub fn pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2658        use petri_artifacts_vmm_test::artifacts::loadable::*;
2659        Firmware::Pcat {
2660            guest,
2661            bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2662            svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2663            ide_controllers: [[None, None], [None, None]],
2664        }
2665    }
2666
2667    /// Constructs a standard [`Firmware::OpenhclPcat`] configuration.
2668    pub fn openhcl_pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2669        use petri_artifacts_vmm_test::artifacts::loadable::*;
2670        use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2671        Firmware::OpenhclPcat {
2672            guest,
2673            igvm_path: resolver.require(LATEST_STANDARD_X64).erase(),
2674            bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2675            svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2676            openhcl_config: OpenHclConfig {
2677                // VMBUS redirect is necessary for IDE to be provided by VTL2
2678                vmbus_redirect: true,
2679                ..Default::default()
2680            },
2681        }
2682    }
2683
2684    /// Constructs a standard [`Firmware::Uefi`] configuration.
2685    pub fn uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch, guest: UefiGuest) -> Self {
2686        use petri_artifacts_vmm_test::artifacts::loadable::*;
2687        let uefi_firmware = match arch {
2688            MachineArch::X86_64 => resolver.require(UEFI_FIRMWARE_X64).erase(),
2689            MachineArch::Aarch64 => resolver.require(UEFI_FIRMWARE_AARCH64).erase(),
2690        };
2691        Firmware::Uefi {
2692            guest,
2693            uefi_firmware,
2694            uefi_config: Default::default(),
2695        }
2696    }
2697
2698    /// Constructs a standard [`Firmware::OpenhclUefi`] configuration.
2699    pub fn openhcl_uefi(
2700        resolver: &ArtifactResolver<'_>,
2701        arch: MachineArch,
2702        guest: UefiGuest,
2703        isolation: Option<IsolationType>,
2704    ) -> Self {
2705        use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2706        let igvm_path = match arch {
2707            MachineArch::X86_64 if isolation.is_some() => resolver.require(LATEST_CVM_X64).erase(),
2708            MachineArch::X86_64 => resolver.require(LATEST_STANDARD_X64).erase(),
2709            MachineArch::Aarch64 => resolver.require(LATEST_STANDARD_AARCH64).erase(),
2710        };
2711        Firmware::OpenhclUefi {
2712            guest,
2713            isolation,
2714            igvm_path,
2715            uefi_config: Default::default(),
2716            openhcl_config: Default::default(),
2717        }
2718    }
2719
2720    fn is_openhcl(&self) -> bool {
2721        match self {
2722            Firmware::OpenhclLinuxDirect { .. }
2723            | Firmware::OpenhclUefi { .. }
2724            | Firmware::OpenhclPcat { .. } => true,
2725            Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => false,
2726        }
2727    }
2728
2729    fn isolation(&self) -> Option<IsolationType> {
2730        match self {
2731            Firmware::OpenhclUefi { isolation, .. } => *isolation,
2732            Firmware::LinuxDirect { .. }
2733            | Firmware::Pcat { .. }
2734            | Firmware::Uefi { .. }
2735            | Firmware::OpenhclLinuxDirect { .. }
2736            | Firmware::OpenhclPcat { .. } => None,
2737        }
2738    }
2739
2740    fn is_linux_direct(&self) -> bool {
2741        match self {
2742            Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => true,
2743            Firmware::Pcat { .. }
2744            | Firmware::Uefi { .. }
2745            | Firmware::OpenhclUefi { .. }
2746            | Firmware::OpenhclPcat { .. } => false,
2747        }
2748    }
2749
2750    /// Get the initrd path for Linux direct boot firmware.
2751    pub fn linux_direct_initrd(&self) -> Option<&Path> {
2752        match self {
2753            Firmware::LinuxDirect { initrd, .. } => Some(initrd.get()),
2754            _ => None,
2755        }
2756    }
2757
2758    fn is_pcat(&self) -> bool {
2759        match self {
2760            Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => true,
2761            Firmware::Uefi { .. }
2762            | Firmware::OpenhclUefi { .. }
2763            | Firmware::LinuxDirect { .. }
2764            | Firmware::OpenhclLinuxDirect { .. } => false,
2765        }
2766    }
2767
2768    fn os_flavor(&self) -> OsFlavor {
2769        match self {
2770            Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => OsFlavor::Linux,
2771            Firmware::Uefi {
2772                guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2773                ..
2774            }
2775            | Firmware::OpenhclUefi {
2776                guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2777                ..
2778            } => OsFlavor::Uefi,
2779            Firmware::Pcat {
2780                guest: PcatGuest::Vhd(cfg),
2781                ..
2782            }
2783            | Firmware::OpenhclPcat {
2784                guest: PcatGuest::Vhd(cfg),
2785                ..
2786            }
2787            | Firmware::Uefi {
2788                guest: UefiGuest::Vhd(cfg),
2789                ..
2790            }
2791            | Firmware::OpenhclUefi {
2792                guest: UefiGuest::Vhd(cfg),
2793                ..
2794            } => cfg.os_flavor,
2795            Firmware::Pcat {
2796                guest: PcatGuest::Iso(cfg),
2797                ..
2798            }
2799            | Firmware::OpenhclPcat {
2800                guest: PcatGuest::Iso(cfg),
2801                ..
2802            } => cfg.os_flavor,
2803        }
2804    }
2805
2806    fn quirks(&self) -> GuestQuirks {
2807        match self {
2808            Firmware::Pcat {
2809                guest: PcatGuest::Vhd(cfg),
2810                ..
2811            }
2812            | Firmware::Uefi {
2813                guest: UefiGuest::Vhd(cfg),
2814                ..
2815            }
2816            | Firmware::OpenhclUefi {
2817                guest: UefiGuest::Vhd(cfg),
2818                ..
2819            } => cfg.quirks.clone(),
2820            Firmware::Pcat {
2821                guest: PcatGuest::Iso(cfg),
2822                ..
2823            } => cfg.quirks.clone(),
2824            _ => Default::default(),
2825        }
2826    }
2827
2828    fn expected_boot_event(&self) -> Option<FirmwareEvent> {
2829        match self {
2830            Firmware::LinuxDirect { .. }
2831            | Firmware::OpenhclLinuxDirect { .. }
2832            | Firmware::Uefi {
2833                guest: UefiGuest::GuestTestUefi(_),
2834                ..
2835            }
2836            | Firmware::OpenhclUefi {
2837                guest: UefiGuest::GuestTestUefi(_),
2838                ..
2839            } => None,
2840            Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => {
2841                // TODO: Handle older PCAT versions that don't fire the event
2842                Some(FirmwareEvent::BootAttempt)
2843            }
2844            Firmware::Uefi {
2845                guest: UefiGuest::None,
2846                ..
2847            }
2848            | Firmware::OpenhclUefi {
2849                guest: UefiGuest::None,
2850                ..
2851            } => Some(FirmwareEvent::NoBootDevice),
2852            Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => {
2853                Some(FirmwareEvent::BootSuccess)
2854            }
2855        }
2856    }
2857
2858    fn openhcl_config(&self) -> Option<&OpenHclConfig> {
2859        match self {
2860            Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2861            | Firmware::OpenhclUefi { openhcl_config, .. }
2862            | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
2863            Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2864        }
2865    }
2866
2867    fn openhcl_config_mut(&mut self) -> Option<&mut OpenHclConfig> {
2868        match self {
2869            Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2870            | Firmware::OpenhclUefi { openhcl_config, .. }
2871            | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
2872            Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2873        }
2874    }
2875
2876    #[cfg_attr(not(windows), expect(dead_code))]
2877    fn openhcl_firmware(&self) -> Option<&Path> {
2878        match self {
2879            Firmware::OpenhclLinuxDirect { igvm_path, .. }
2880            | Firmware::OpenhclUefi { igvm_path, .. }
2881            | Firmware::OpenhclPcat { igvm_path, .. } => Some(igvm_path.get()),
2882            Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2883        }
2884    }
2885
2886    fn into_runtime_config(
2887        self,
2888        vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
2889    ) -> PetriVmRuntimeConfig {
2890        match self {
2891            Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2892            | Firmware::OpenhclUefi { openhcl_config, .. }
2893            | Firmware::OpenhclPcat { openhcl_config, .. } => PetriVmRuntimeConfig {
2894                vtl2_settings: Some(
2895                    openhcl_config
2896                        .vtl2_settings
2897                        .unwrap_or_else(default_vtl2_settings),
2898                ),
2899                ide_controllers: None,
2900                vmbus_storage_controllers,
2901            },
2902            Firmware::Pcat {
2903                ide_controllers, ..
2904            } => PetriVmRuntimeConfig {
2905                vtl2_settings: None,
2906                ide_controllers: Some(ide_controllers),
2907                vmbus_storage_controllers,
2908            },
2909            Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } => PetriVmRuntimeConfig {
2910                vtl2_settings: None,
2911                ide_controllers: None,
2912                vmbus_storage_controllers,
2913            },
2914        }
2915    }
2916
2917    fn uefi_config(&self) -> Option<&UefiConfig> {
2918        match self {
2919            Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
2920                Some(uefi_config)
2921            }
2922            Firmware::LinuxDirect { .. }
2923            | Firmware::OpenhclLinuxDirect { .. }
2924            | Firmware::Pcat { .. }
2925            | Firmware::OpenhclPcat { .. } => None,
2926        }
2927    }
2928
2929    fn uefi_config_mut(&mut self) -> Option<&mut UefiConfig> {
2930        match self {
2931            Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
2932                Some(uefi_config)
2933            }
2934            Firmware::LinuxDirect { .. }
2935            | Firmware::OpenhclLinuxDirect { .. }
2936            | Firmware::Pcat { .. }
2937            | Firmware::OpenhclPcat { .. } => None,
2938        }
2939    }
2940
2941    fn boot_drive(&self) -> Option<Drive> {
2942        match self {
2943            Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => None,
2944            Firmware::Pcat { guest, .. } | Firmware::OpenhclPcat { guest, .. } => {
2945                Some((guest.disk_path(), guest.is_dvd()))
2946            }
2947            Firmware::Uefi { guest, .. } | Firmware::OpenhclUefi { guest, .. } => {
2948                guest.disk_path().map(|dp| (dp, false))
2949            }
2950        }
2951        .map(|(disk_path, is_dvd)| Drive::new(Some(Disk::Differencing(disk_path)), is_dvd))
2952    }
2953
2954    fn vtl2_settings(&mut self) -> Option<&mut Vtl2Settings> {
2955        self.openhcl_config_mut()
2956            .map(|c| c.vtl2_settings.get_or_insert_with(default_vtl2_settings))
2957    }
2958
2959    fn ide_controllers(&self) -> Option<&[[Option<Drive>; 2]; 2]> {
2960        match self {
2961            Firmware::Pcat {
2962                ide_controllers, ..
2963            } => Some(ide_controllers),
2964            _ => None,
2965        }
2966    }
2967
2968    fn ide_controllers_mut(&mut self) -> Option<&mut [[Option<Drive>; 2]; 2]> {
2969        match self {
2970            Firmware::Pcat {
2971                ide_controllers, ..
2972            } => Some(ide_controllers),
2973            _ => None,
2974        }
2975    }
2976}
2977
2978/// The guest the VM will boot into. A boot drive with the chosen setup
2979/// will be automatically configured.
2980#[derive(Debug)]
2981pub enum PcatGuest {
2982    /// Mount a VHD as the boot drive.
2983    Vhd(BootImageConfig<boot_image_type::Vhd>),
2984    /// Mount an ISO as the CD/DVD drive.
2985    Iso(BootImageConfig<boot_image_type::Iso>),
2986}
2987
2988impl PcatGuest {
2989    fn disk_path(&self) -> DiskPath {
2990        match self {
2991            PcatGuest::Vhd(disk) => disk.disk_path(),
2992            PcatGuest::Iso(disk) => disk.disk_path(),
2993        }
2994    }
2995
2996    fn is_dvd(&self) -> bool {
2997        matches!(self, Self::Iso(_))
2998    }
2999}
3000
3001/// The guest the VM will boot into. A boot drive with the chosen setup
3002/// will be automatically configured.
3003#[derive(Debug)]
3004pub enum UefiGuest {
3005    /// Mount a VHD as the boot drive.
3006    Vhd(BootImageConfig<boot_image_type::Vhd>),
3007    /// The UEFI test image produced by our guest-test infrastructure.
3008    GuestTestUefi(ResolvedArtifact),
3009    /// No guest, just the firmware.
3010    None,
3011}
3012
3013impl UefiGuest {
3014    /// Construct a standard [`UefiGuest::GuestTestUefi`] configuration.
3015    pub fn guest_test_uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
3016        use petri_artifacts_vmm_test::artifacts::test_vhd::*;
3017        let artifact = match arch {
3018            MachineArch::X86_64 => resolver.require(GUEST_TEST_UEFI_X64).erase(),
3019            MachineArch::Aarch64 => resolver.require(GUEST_TEST_UEFI_AARCH64).erase(),
3020        };
3021        UefiGuest::GuestTestUefi(artifact)
3022    }
3023
3024    fn disk_path(&self) -> Option<DiskPath> {
3025        match self {
3026            UefiGuest::Vhd(vhd) => Some(vhd.disk_path()),
3027            UefiGuest::GuestTestUefi(p) => Some(DiskPath::Local(p.get().to_path_buf())),
3028            UefiGuest::None => None,
3029        }
3030    }
3031}
3032
3033/// Type-tags for [`BootImageConfig`](super::BootImageConfig)
3034pub mod boot_image_type {
3035    mod private {
3036        pub trait Sealed {}
3037        impl Sealed for super::Vhd {}
3038        impl Sealed for super::Iso {}
3039    }
3040
3041    /// Private trait use to seal the set of artifact types BootImageType
3042    /// supports.
3043    pub trait BootImageType: private::Sealed {}
3044
3045    /// BootImageConfig for a VHD file
3046    #[derive(Debug)]
3047    pub enum Vhd {}
3048
3049    /// BootImageConfig for an ISO file
3050    #[derive(Debug)]
3051    pub enum Iso {}
3052
3053    impl BootImageType for Vhd {}
3054    impl BootImageType for Iso {}
3055}
3056
3057/// Configuration information for the boot drive of the VM.
3058#[derive(Debug)]
3059pub struct BootImageConfig<T: boot_image_type::BootImageType> {
3060    /// Artifact source corresponding to the boot media (local or remote).
3061    artifact: ResolvedArtifactSource,
3062    /// The OS flavor.
3063    os_flavor: OsFlavor,
3064    /// Any quirks needed to boot the guest.
3065    ///
3066    /// Most guests should not need any quirks, and can use `Default`.
3067    quirks: GuestQuirks,
3068    /// Marker denoting what type of media `artifact` corresponds to
3069    _type: core::marker::PhantomData<T>,
3070}
3071
3072impl<T: boot_image_type::BootImageType> BootImageConfig<T> {
3073    /// Get a [`DiskPath`] from the artifact source.
3074    fn disk_path(&self) -> DiskPath {
3075        match self.artifact.get() {
3076            ArtifactSource::Local(p) => DiskPath::Local(p.clone()),
3077            ArtifactSource::Remote { url } => DiskPath::Remote { url: url.clone() },
3078        }
3079    }
3080}
3081
3082impl BootImageConfig<boot_image_type::Vhd> {
3083    /// Create a new BootImageConfig from a VHD artifact source
3084    pub fn from_vhd<A>(artifact: ResolvedArtifactSource<A>) -> Self
3085    where
3086        A: petri_artifacts_common::tags::IsTestVhd,
3087    {
3088        BootImageConfig {
3089            artifact: artifact.erase(),
3090            os_flavor: A::OS_FLAVOR,
3091            quirks: A::quirks(),
3092            _type: std::marker::PhantomData,
3093        }
3094    }
3095}
3096
3097impl BootImageConfig<boot_image_type::Iso> {
3098    /// Create a new BootImageConfig from an ISO artifact source
3099    pub fn from_iso<A>(artifact: ResolvedArtifactSource<A>) -> Self
3100    where
3101        A: petri_artifacts_common::tags::IsTestIso,
3102    {
3103        BootImageConfig {
3104            artifact: artifact.erase(),
3105            os_flavor: A::OS_FLAVOR,
3106            quirks: A::quirks(),
3107            _type: std::marker::PhantomData,
3108        }
3109    }
3110}
3111
3112/// Isolation type
3113#[derive(Debug, Clone, Copy)]
3114pub enum IsolationType {
3115    /// VBS
3116    Vbs,
3117    /// SNP
3118    Snp,
3119    /// TDX
3120    Tdx,
3121}
3122
3123/// Flags controlling servicing behavior.
3124#[derive(Debug, Clone, Copy)]
3125pub struct OpenHclServicingFlags {
3126    /// Preserve DMA memory for NVMe devices if supported.
3127    /// Defaults to `true`.
3128    pub enable_nvme_keepalive: bool,
3129    /// Preserve DMA memory for MANA devices if supported.
3130    pub enable_mana_keepalive: bool,
3131    /// 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.
3132    pub override_version_checks: bool,
3133    /// Hint to the OpenHCL runtime how much time to wait when stopping / saving the OpenHCL.
3134    pub stop_timeout_hint_secs: Option<u16>,
3135}
3136
3137/// Where a disk image is located.
3138#[derive(Debug, Clone)]
3139pub enum DiskPath {
3140    /// A local file path.
3141    Local(PathBuf),
3142    /// A remote URL (fetched on demand via HTTP Range requests).
3143    Remote {
3144        /// The URL where the disk can be fetched.
3145        url: String,
3146    },
3147}
3148
3149impl From<PathBuf> for DiskPath {
3150    fn from(path: PathBuf) -> Self {
3151        DiskPath::Local(path)
3152    }
3153}
3154
3155/// Petri disk
3156#[derive(Debug, Clone)]
3157pub enum Disk {
3158    /// Memory backed with specified size
3159    Memory(u64),
3160    /// Memory differencing disk backed by a VHD (local or remote)
3161    Differencing(DiskPath),
3162    /// Persistent VHD
3163    Persistent(PathBuf),
3164    /// Disk backed by a temporary VHD
3165    Temporary(Arc<TempPath>),
3166}
3167
3168/// Petri VMGS disk
3169#[derive(Debug, Clone)]
3170pub struct PetriVmgsDisk {
3171    /// Backing disk
3172    pub disk: Disk,
3173    /// Guest state encryption policy
3174    pub encryption_policy: GuestStateEncryptionPolicy,
3175}
3176
3177impl Default for PetriVmgsDisk {
3178    fn default() -> Self {
3179        PetriVmgsDisk {
3180            disk: Disk::Memory(vmgs_format::VMGS_DEFAULT_CAPACITY),
3181            // TODO: make this strict once we can set it in OpenHCL on Hyper-V
3182            encryption_policy: GuestStateEncryptionPolicy::None(false),
3183        }
3184    }
3185}
3186
3187/// Petri VM guest state resource
3188#[derive(Debug, Clone)]
3189pub enum PetriVmgsResource {
3190    /// Use disk to store guest state
3191    Disk(PetriVmgsDisk),
3192    /// Use disk to store guest state, reformatting if corrupted.
3193    ReprovisionOnFailure(PetriVmgsDisk),
3194    /// Format and use disk to store guest state
3195    Reprovision(PetriVmgsDisk),
3196    /// Store guest state in memory
3197    Ephemeral,
3198}
3199
3200impl PetriVmgsResource {
3201    /// get the inner vmgs disk if one exists
3202    pub fn vmgs(&self) -> Option<&PetriVmgsDisk> {
3203        match self {
3204            PetriVmgsResource::Disk(vmgs)
3205            | PetriVmgsResource::ReprovisionOnFailure(vmgs)
3206            | PetriVmgsResource::Reprovision(vmgs) => Some(vmgs),
3207            PetriVmgsResource::Ephemeral => None,
3208        }
3209    }
3210
3211    /// get the inner disk if one exists
3212    pub fn disk(&self) -> Option<&Disk> {
3213        self.vmgs().map(|vmgs| &vmgs.disk)
3214    }
3215
3216    /// get the encryption policy of the vmgs
3217    pub fn encryption_policy(&self) -> Option<GuestStateEncryptionPolicy> {
3218        self.vmgs().map(|vmgs| vmgs.encryption_policy)
3219    }
3220}
3221
3222/// Petri VM guest state lifetime
3223#[derive(Debug, Clone, Copy)]
3224pub enum PetriGuestStateLifetime {
3225    /// Use a differencing disk backed by a blank, tempory VMGS file
3226    /// or other artifact if one is provided
3227    Disk,
3228    /// Same as default, except reformat the backing disk if corrupted
3229    ReprovisionOnFailure,
3230    /// Same as default, except reformat the backing disk
3231    Reprovision,
3232    /// Store guest state in memory (no backing disk)
3233    Ephemeral,
3234}
3235
3236/// UEFI secure boot template
3237#[derive(Debug, Clone, Copy)]
3238pub enum SecureBootTemplate {
3239    /// The Microsoft Windows template.
3240    MicrosoftWindows,
3241    /// The Microsoft UEFI certificate authority template.
3242    MicrosoftUefiCertificateAuthority,
3243}
3244
3245/// Quirks to workaround certain bugs that only manifest when using a
3246/// particular VMM, and do not depend on which guest is running.
3247#[derive(Default, Debug, Clone)]
3248pub struct VmmQuirks {
3249    /// Automatically reset the VM if we did not recieve a boot event in the
3250    /// specified amount of time.
3251    pub flaky_boot: Option<Duration>,
3252}
3253
3254/// Creates a VM-safe name that respects platform limitations.
3255///
3256/// Hyper-V limits VM names to 100 characters. For names that exceed this limit,
3257/// this function truncates to 96 characters and appends a 4-character hash
3258/// to ensure uniqueness while staying within the limit.
3259fn make_vm_safe_name(name: &str) -> String {
3260    const MAX_VM_NAME_LENGTH: usize = 100;
3261    const HASH_LENGTH: usize = 4;
3262    const MAX_PREFIX_LENGTH: usize = MAX_VM_NAME_LENGTH - HASH_LENGTH;
3263
3264    if name.len() <= MAX_VM_NAME_LENGTH {
3265        name.to_owned()
3266    } else {
3267        // Create a hash of the full name for uniqueness
3268        let mut hasher = DefaultHasher::new();
3269        name.hash(&mut hasher);
3270        let hash = hasher.finish();
3271
3272        // Format hash as a 4-character hex string
3273        let hash_suffix = format!("{:04x}", hash & 0xFFFF);
3274
3275        // Truncate the name and append the hash
3276        let truncated = &name[..MAX_PREFIX_LENGTH];
3277        tracing::debug!(
3278            "VM name too long ({}), truncating '{}' to '{}{}'",
3279            name.len(),
3280            name,
3281            truncated,
3282            hash_suffix
3283        );
3284
3285        format!("{}{}", truncated, hash_suffix)
3286    }
3287}
3288
3289/// The reason that the VM halted
3290#[derive(Debug, Clone, Copy, Eq, PartialEq)]
3291pub enum PetriHaltReason {
3292    /// The vm powered off
3293    PowerOff,
3294    /// The vm reset
3295    Reset,
3296    /// The vm hibernated
3297    Hibernate,
3298    /// The vm triple faulted
3299    TripleFault,
3300    /// The vm halted for some other reason
3301    Other,
3302}
3303
3304impl PetriHaltReason {
3305    /// Construct a halt reason with detailed debug info
3306    pub fn with_detail(self, detail: String) -> PetriHaltReasonDetail {
3307        PetriHaltReasonDetail {
3308            reason: self,
3309            detail,
3310        }
3311    }
3312}
3313
3314/// The reason that the VM halted, with optional addition debug details
3315#[derive(Debug, Clone)]
3316pub struct PetriHaltReasonDetail {
3317    /// The reason for the halt
3318    pub reason: PetriHaltReason,
3319    /// More details about the halt
3320    pub detail: String,
3321}
3322
3323fn append_cmdline(cmd: &mut Option<String>, add_cmd: impl AsRef<str>) {
3324    if let Some(cmd) = cmd.as_mut() {
3325        cmd.push(' ');
3326        cmd.push_str(add_cmd.as_ref());
3327    } else {
3328        *cmd = Some(add_cmd.as_ref().to_string());
3329    }
3330}
3331
3332async fn save_inspect(
3333    name: &str,
3334    inspect: std::pin::Pin<Box<dyn Future<Output = anyhow::Result<inspect::Node>> + Send>>,
3335    log_source: &PetriLogSource,
3336) {
3337    tracing::info!("Collecting {name} inspect details.");
3338    let node = match inspect.await {
3339        Ok(n) => n,
3340        Err(e) => {
3341            tracing::error!(?e, "Failed to get {name}");
3342            return;
3343        }
3344    };
3345    if let Err(e) = log_source.write_attachment(
3346        &format!("timeout_inspect_{name}.log"),
3347        format!("{node:#}").as_bytes(),
3348    ) {
3349        tracing::error!(?e, "Failed to save {name} inspect log");
3350        return;
3351    }
3352    tracing::info!("{name} inspect task finished.");
3353}
3354
3355/// Wrapper for modification functions with stubbed out debug impl
3356pub struct ModifyFn<T>(pub Box<dyn FnOnce(T) -> T + Send>);
3357
3358impl<T> Debug for ModifyFn<T> {
3359    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3360        write!(f, "_")
3361    }
3362}
3363
3364/// Default VTL 2 settings used by petri
3365fn default_vtl2_settings() -> Vtl2Settings {
3366    Vtl2Settings {
3367        version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
3368        fixed: None,
3369        dynamic: Some(Default::default()),
3370        namespace_settings: Default::default(),
3371    }
3372}
3373
3374/// Virtual trust level
3375#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3376pub enum Vtl {
3377    /// VTL 0
3378    Vtl0 = 0,
3379    /// VTL 1
3380    Vtl1 = 1,
3381    /// VTL 2
3382    Vtl2 = 2,
3383}
3384
3385/// The VMBus storage device type.
3386#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3387pub enum VmbusStorageType {
3388    /// SCSI
3389    Scsi,
3390    /// NVMe
3391    Nvme,
3392    /// Virtio block device
3393    VirtioBlk,
3394}
3395
3396/// VM disk drive
3397#[derive(Debug, Clone)]
3398pub struct Drive {
3399    /// Backing disk
3400    pub disk: Option<Disk>,
3401    /// Whether this is a DVD
3402    pub is_dvd: bool,
3403}
3404
3405impl Drive {
3406    /// Create a new disk
3407    pub fn new(disk: Option<Disk>, is_dvd: bool) -> Self {
3408        Self { disk, is_dvd }
3409    }
3410}
3411
3412/// VMBus storage controller
3413#[derive(Debug, Clone)]
3414pub struct VmbusStorageController {
3415    /// The VTL to assign the storage controller to
3416    pub target_vtl: Vtl,
3417    /// The storage device type
3418    pub controller_type: VmbusStorageType,
3419    /// Drives (with any inserted disks) attached to this storage controller
3420    pub drives: HashMap<u32, Drive>,
3421}
3422
3423impl VmbusStorageController {
3424    /// Create a new storage controller
3425    pub fn new(target_vtl: Vtl, controller_type: VmbusStorageType) -> Self {
3426        Self {
3427            target_vtl,
3428            controller_type,
3429            drives: HashMap::new(),
3430        }
3431    }
3432
3433    /// Add a disk to the storage controller
3434    pub fn set_drive(
3435        &mut self,
3436        lun: Option<u32>,
3437        drive: Drive,
3438        allow_modify_existing: bool,
3439    ) -> u32 {
3440        let lun = lun.unwrap_or_else(|| {
3441            // find the first available lun
3442            let mut lun = None;
3443            for x in 0..u8::MAX as u32 {
3444                if !self.drives.contains_key(&x) {
3445                    lun = Some(x);
3446                    break;
3447                }
3448            }
3449            lun.expect("all locations on this controller are in use")
3450        });
3451
3452        if self.drives.insert(lun, drive).is_some() && !allow_modify_existing {
3453            panic!("a disk with lun {lun} already existed on this controller");
3454        }
3455
3456        lun
3457    }
3458}
3459
3460/// Returns the cache directory for lazy-fetched disk artifacts.
3461pub(crate) fn petri_disk_cache_dir() -> String {
3462    if let Ok(dir) = std::env::var("PETRI_CACHE_DIR") {
3463        return dir;
3464    }
3465
3466    #[cfg(target_os = "macos")]
3467    {
3468        if let Ok(home) = std::env::var("HOME") {
3469            return format!("{home}/Library/Caches/petri");
3470        }
3471    }
3472
3473    #[cfg(windows)]
3474    {
3475        if let Ok(local) = std::env::var("LOCALAPPDATA") {
3476            return format!("{local}\\petri\\cache");
3477        }
3478    }
3479
3480    // Linux / fallback: XDG
3481    if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
3482        return format!("{xdg}/petri");
3483    }
3484    if let Ok(home) = std::env::var("HOME") {
3485        return format!("{home}/.cache/petri");
3486    }
3487
3488    ".cache/petri".to_string()
3489}
3490
3491#[cfg(test)]
3492mod tests {
3493    use super::make_vm_safe_name;
3494    use crate::Drive;
3495    use crate::VmbusStorageController;
3496    use crate::VmbusStorageType;
3497    use crate::Vtl;
3498
3499    #[test]
3500    fn test_short_names_unchanged() {
3501        let short_name = "short_test_name";
3502        assert_eq!(make_vm_safe_name(short_name), short_name);
3503    }
3504
3505    #[test]
3506    fn test_exactly_100_chars_unchanged() {
3507        let name_100 = "a".repeat(100);
3508        assert_eq!(make_vm_safe_name(&name_100), name_100);
3509    }
3510
3511    #[test]
3512    fn test_long_name_truncated() {
3513        let long_name = "multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_openhcl_servicing";
3514        let result = make_vm_safe_name(long_name);
3515
3516        // Should be exactly 100 characters
3517        assert_eq!(result.len(), 100);
3518
3519        // Should start with the truncated prefix
3520        assert!(result.starts_with("multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_ope"));
3521
3522        // Should end with a 4-character hash
3523        let suffix = &result[96..];
3524        assert_eq!(suffix.len(), 4);
3525        // Should be valid hex
3526        assert!(u16::from_str_radix(suffix, 16).is_ok());
3527    }
3528
3529    #[test]
3530    fn test_deterministic_results() {
3531        let long_name = "very_long_test_name_that_exceeds_the_100_character_limit_and_should_be_truncated_consistently_every_time";
3532        let result1 = make_vm_safe_name(long_name);
3533        let result2 = make_vm_safe_name(long_name);
3534
3535        assert_eq!(result1, result2);
3536        assert_eq!(result1.len(), 100);
3537    }
3538
3539    #[test]
3540    fn test_different_names_different_hashes() {
3541        let name1 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_1";
3542        let name2 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_2";
3543
3544        let result1 = make_vm_safe_name(name1);
3545        let result2 = make_vm_safe_name(name2);
3546
3547        // Both should be 100 chars
3548        assert_eq!(result1.len(), 100);
3549        assert_eq!(result2.len(), 100);
3550
3551        // Should have different suffixes since the full names are different
3552        assert_ne!(result1, result2);
3553        assert_ne!(&result1[96..], &result2[96..]);
3554    }
3555
3556    #[test]
3557    fn test_vmbus_storage_controller() {
3558        let mut controller = VmbusStorageController::new(Vtl::Vtl0, VmbusStorageType::Scsi);
3559        assert_eq!(
3560            controller.set_drive(Some(1), Drive::new(None, false), false),
3561            1
3562        );
3563        assert!(controller.drives.contains_key(&1));
3564        assert_eq!(
3565            controller.set_drive(None, Drive::new(None, false), false),
3566            0
3567        );
3568        assert!(controller.drives.contains_key(&0));
3569        assert_eq!(
3570            controller.set_drive(None, Drive::new(None, false), false),
3571            2
3572        );
3573        assert!(controller.drives.contains_key(&2));
3574        assert_eq!(
3575            controller.set_drive(Some(0), Drive::new(None, false), true),
3576            0
3577        );
3578        assert!(controller.drives.contains_key(&0));
3579    }
3580}