petri/vm/
mod.rs

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