petri/vm/
mod.rs

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