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