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