Skip to main content

petri/vm/
mod.rs

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