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