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 async_trait::async_trait;
18use get_resources::ged::FirmwareEvent;
19use hvlite_defs::config::Vtl2BaseAddressType;
20use mesh::CancelContext;
21use pal_async::DefaultDriver;
22use pal_async::task::Spawn;
23use pal_async::task::Task;
24use pal_async::timer::PolledTimer;
25use petri_artifacts_common::tags::GuestQuirks;
26use petri_artifacts_common::tags::GuestQuirksInner;
27use petri_artifacts_common::tags::InitialRebootCondition;
28use petri_artifacts_common::tags::IsOpenhclIgvm;
29use petri_artifacts_common::tags::IsTestVmgs;
30use petri_artifacts_common::tags::MachineArch;
31use petri_artifacts_common::tags::OsFlavor;
32use petri_artifacts_core::ArtifactResolver;
33use petri_artifacts_core::ResolvedArtifact;
34use petri_artifacts_core::ResolvedOptionalArtifact;
35use pipette_client::PipetteClient;
36use std::collections::hash_map::DefaultHasher;
37use std::hash::Hash;
38use std::hash::Hasher;
39use std::path::Path;
40use std::path::PathBuf;
41use std::sync::Arc;
42use std::time::Duration;
43use tempfile::TempPath;
44use vmgs_resources::GuestStateEncryptionPolicy;
45
46/// The set of artifacts and resources needed to instantiate a
47/// [`PetriVmBuilder`].
48pub struct PetriVmArtifacts<T: PetriVmmBackend> {
49    /// Artifacts needed to launch the host VMM used for the test
50    pub backend: T,
51    /// Firmware and/or OS to load into the VM and associated settings
52    pub firmware: Firmware,
53    /// The architecture of the VM
54    pub arch: MachineArch,
55    /// Agent to run in the guest
56    pub agent_image: Option<AgentImage>,
57    /// Agent to run in OpenHCL
58    pub openhcl_agent_image: Option<AgentImage>,
59}
60
61impl<T: PetriVmmBackend> PetriVmArtifacts<T> {
62    /// Resolves the artifacts needed to instantiate a [`PetriVmBuilder`].
63    ///
64    /// Returns `None` if the supplied configuration is not supported on this platform.
65    pub fn new(
66        resolver: &ArtifactResolver<'_>,
67        firmware: Firmware,
68        arch: MachineArch,
69        with_vtl0_pipette: bool,
70    ) -> Option<Self> {
71        if !T::check_compat(&firmware, arch) {
72            return None;
73        }
74
75        Some(Self {
76            backend: T::new(resolver),
77            arch,
78            agent_image: Some(if with_vtl0_pipette {
79                AgentImage::new(firmware.os_flavor()).with_pipette(resolver, arch)
80            } else {
81                AgentImage::new(firmware.os_flavor())
82            }),
83            openhcl_agent_image: if firmware.is_openhcl() {
84                Some(AgentImage::new(OsFlavor::Linux).with_pipette(resolver, arch))
85            } else {
86                None
87            },
88            firmware,
89        })
90    }
91}
92
93/// Petri VM builder
94pub struct PetriVmBuilder<T: PetriVmmBackend> {
95    /// Artifacts needed to launch the host VMM used for the test
96    backend: T,
97    /// VM configuration
98    config: PetriVmConfig,
99    /// Function to modify the VMM-specific configuration
100    modify_vmm_config: Option<Box<dyn FnOnce(T::VmmConfig) -> T::VmmConfig + Send>>,
101    /// VMM-agnostic resources
102    resources: PetriVmResources,
103
104    // VMM-specific quirks for the configured firmware
105    guest_quirks: GuestQuirksInner,
106    vmm_quirks: VmmQuirks,
107
108    // Test-specific boot behavior expectations.
109    // Defaults to expected behavior for firmware configuration.
110    expected_boot_event: Option<FirmwareEvent>,
111    override_expect_reset: bool,
112}
113
114/// Petri VM configuration
115pub struct PetriVmConfig {
116    /// The name of the VM
117    pub name: String,
118    /// The architecture of the VM
119    pub arch: MachineArch,
120    /// Firmware and/or OS to load into the VM and associated settings
121    pub firmware: Firmware,
122    /// The amount of memory, in bytes, to assign to the VM
123    pub memory: MemoryConfig,
124    /// The processor tology for the VM
125    pub proc_topology: ProcessorTopology,
126    /// Agent to run in the guest
127    pub agent_image: Option<AgentImage>,
128    /// Agent to run in OpenHCL
129    pub openhcl_agent_image: Option<AgentImage>,
130    /// Disk to use for guest crash dumps
131    pub guest_crash_disk: Option<Arc<TempPath>>,
132    /// VM guest state
133    pub vmgs: PetriVmgsResource,
134    /// The boot device type for the VM
135    pub boot_device_type: BootDeviceType,
136    /// Configure TPM state persistence
137    pub tpm_state_persistence: bool,
138}
139
140/// Resources used by a Petri VM during contruction and runtime
141pub struct PetriVmResources {
142    driver: DefaultDriver,
143    log_source: PetriLogSource,
144}
145
146/// Trait for VMM-specific contruction and runtime resources
147#[async_trait]
148pub trait PetriVmmBackend {
149    /// VMM-specific configuration
150    type VmmConfig;
151
152    /// Runtime object
153    type VmRuntime: PetriVmRuntime;
154
155    /// Check whether the combination of firmware and architecture is
156    /// supported on the VMM.
157    fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool;
158
159    /// Select backend specific quirks guest and vmm quirks.
160    fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks);
161
162    /// Get the default servicing flags (based on what this backend supports)
163    fn default_servicing_flags() -> OpenHclServicingFlags;
164
165    /// Create a disk for guest crash dumps, and a post-test hook to open the disk
166    /// to allow for reading the dumps.
167    fn create_guest_dump_disk() -> anyhow::Result<
168        Option<(
169            Arc<TempPath>,
170            Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
171        )>,
172    >;
173
174    /// Resolve any artifacts needed to use this backend
175    fn new(resolver: &ArtifactResolver<'_>) -> Self;
176
177    /// Create and start VM from the generic config using the VMM backend
178    async fn run(
179        self,
180        config: PetriVmConfig,
181        modify_vmm_config: Option<impl FnOnce(Self::VmmConfig) -> Self::VmmConfig + Send>,
182        resources: &PetriVmResources,
183    ) -> anyhow::Result<Self::VmRuntime>;
184}
185
186pub(crate) const PETRI_VTL0_SCSI_BOOT_LUN: u8 = 0;
187pub(crate) const PETRI_VTL0_SCSI_PIPETTE_LUN: u8 = 1;
188pub(crate) const PETRI_VTL0_SCSI_CRASH_LUN: u8 = 2;
189
190/// A constructed Petri VM
191pub struct PetriVm<T: PetriVmmBackend> {
192    resources: PetriVmResources,
193    runtime: T::VmRuntime,
194    watchdog_tasks: Vec<Task<()>>,
195    openhcl_diag_handler: Option<OpenHclDiagHandler>,
196
197    arch: MachineArch,
198    guest_quirks: GuestQuirksInner,
199    vmm_quirks: VmmQuirks,
200    expected_boot_event: Option<FirmwareEvent>,
201}
202
203impl<T: PetriVmmBackend> PetriVmBuilder<T> {
204    /// Create a new VM configuration.
205    pub fn new(
206        params: PetriTestParams<'_>,
207        artifacts: PetriVmArtifacts<T>,
208        driver: &DefaultDriver,
209    ) -> anyhow::Result<Self> {
210        let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
211        let expected_boot_event = artifacts.firmware.expected_boot_event();
212        let boot_device_type = match artifacts.firmware {
213            Firmware::LinuxDirect { .. } => BootDeviceType::None,
214            Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
215            Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => BootDeviceType::Ide,
216            Firmware::Uefi {
217                guest: UefiGuest::None,
218                ..
219            }
220            | Firmware::OpenhclUefi {
221                guest: UefiGuest::None,
222                ..
223            } => BootDeviceType::None,
224            Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
225        };
226
227        let guest_crash_disk = if matches!(
228            artifacts.firmware.os_flavor(),
229            OsFlavor::Windows | OsFlavor::Linux
230        ) {
231            let (guest_crash_disk, guest_dump_disk_hook) = T::create_guest_dump_disk()?.unzip();
232            if let Some(guest_dump_disk_hook) = guest_dump_disk_hook {
233                let logger = params.logger.clone();
234                params
235                    .post_test_hooks
236                    .push(crate::test::PetriPostTestHook::new(
237                        "extract guest crash dumps".into(),
238                        move |test_passed| {
239                            if test_passed {
240                                return Ok(());
241                            }
242                            let mut disk = guest_dump_disk_hook()?;
243                            let gpt = gptman::GPT::read_from(&mut disk, SECTOR_SIZE)?;
244                            let partition = fscommon::StreamSlice::new(
245                                &mut disk,
246                                gpt[1].starting_lba * SECTOR_SIZE,
247                                gpt[1].ending_lba * SECTOR_SIZE,
248                            )?;
249                            let fs = fatfs::FileSystem::new(partition, fatfs::FsOptions::new())?;
250                            for entry in fs.root_dir().iter() {
251                                let Ok(entry) = entry else {
252                                    tracing::warn!(
253                                        ?entry,
254                                        "failed to read entry in guest crash dump disk"
255                                    );
256                                    continue;
257                                };
258                                if !entry.is_file() {
259                                    tracing::warn!(
260                                        ?entry,
261                                        "skipping non-file entry in guest crash dump disk"
262                                    );
263                                    continue;
264                                }
265                                logger.write_attachment(&entry.file_name(), entry.to_file())?;
266                            }
267                            Ok(())
268                        },
269                    ));
270            }
271            guest_crash_disk
272        } else {
273            None
274        };
275
276        Ok(Self {
277            backend: artifacts.backend,
278            config: PetriVmConfig {
279                name: make_vm_safe_name(params.test_name),
280                arch: artifacts.arch,
281                firmware: artifacts.firmware,
282                boot_device_type,
283                memory: Default::default(),
284                proc_topology: Default::default(),
285                agent_image: artifacts.agent_image,
286                openhcl_agent_image: artifacts.openhcl_agent_image,
287                vmgs: PetriVmgsResource::Ephemeral,
288                tpm_state_persistence: true,
289                guest_crash_disk,
290            },
291            modify_vmm_config: None,
292            resources: PetriVmResources {
293                driver: driver.clone(),
294                log_source: params.logger.clone(),
295            },
296
297            guest_quirks,
298            vmm_quirks,
299            expected_boot_event,
300            override_expect_reset: false,
301        })
302    }
303}
304
305impl<T: PetriVmmBackend> PetriVmBuilder<T> {
306    /// Build and run the VM, then wait for the VM to emit the expected boot
307    /// event (if configured). Does not configure and start pipette. Should
308    /// only be used for testing platforms that pipette does not support.
309    pub async fn run_without_agent(self) -> anyhow::Result<PetriVm<T>> {
310        self.run_core().await
311    }
312
313    /// Build and run the VM, then wait for the VM to emit the expected boot
314    /// event (if configured). Launches pipette and returns a client to it.
315    pub async fn run(self) -> anyhow::Result<(PetriVm<T>, PipetteClient)> {
316        assert!(self.config.agent_image.is_some());
317        assert!(self.config.agent_image.as_ref().unwrap().contains_pipette());
318
319        let mut vm = self.run_core().await?;
320        let client = vm.wait_for_agent().await?;
321        Ok((vm, client))
322    }
323
324    async fn run_core(self) -> anyhow::Result<PetriVm<T>> {
325        let arch = self.config.arch;
326        let expect_reset = self.expect_reset();
327
328        let mut runtime = self
329            .backend
330            .run(self.config, self.modify_vmm_config, &self.resources)
331            .await?;
332        let openhcl_diag_handler = runtime.openhcl_diag();
333        let watchdog_tasks = Self::start_watchdog_tasks(&self.resources, &mut runtime)?;
334
335        let mut vm = PetriVm {
336            resources: self.resources,
337            runtime,
338            watchdog_tasks,
339            openhcl_diag_handler,
340
341            arch,
342            guest_quirks: self.guest_quirks,
343            vmm_quirks: self.vmm_quirks,
344            expected_boot_event: self.expected_boot_event,
345        };
346
347        if expect_reset {
348            vm.wait_for_reset_core().await?;
349        }
350
351        vm.wait_for_expected_boot_event().await?;
352
353        Ok(vm)
354    }
355
356    fn expect_reset(&self) -> bool {
357        // TODO: use presence of TPM here once with_tpm() backend-agnostic.
358        self.override_expect_reset
359            || matches!(
360                (
361                    self.guest_quirks.initial_reboot,
362                    self.expected_boot_event,
363                    &self.config.firmware,
364                ),
365                (
366                    Some(InitialRebootCondition::Always),
367                    Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
368                    _,
369                ) | (
370                    Some(InitialRebootCondition::WithOpenHclUefi),
371                    Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
372                    Firmware::OpenhclUefi { .. },
373                )
374            )
375    }
376
377    fn start_watchdog_tasks(
378        resources: &PetriVmResources,
379        runtime: &mut T::VmRuntime,
380    ) -> anyhow::Result<Vec<Task<()>>> {
381        let mut tasks = Vec::new();
382
383        {
384            const TIMEOUT_DURATION_MINUTES: u64 = 10;
385            const TIMER_DURATION: Duration = Duration::from_secs(TIMEOUT_DURATION_MINUTES * 60);
386            let log_source = resources.log_source.clone();
387            let inspect_task =
388                |name,
389                 driver: &DefaultDriver,
390                 inspect: std::pin::Pin<Box<dyn Future<Output = _> + Send>>| {
391                    driver.spawn(format!("petri-watchdog-inspect-{name}"), async move {
392                        save_inspect(name, inspect, &log_source).await;
393                    })
394                };
395
396            let driver = resources.driver.clone();
397            let vmm_inspector = runtime.inspector();
398            let openhcl_diag_handler = runtime.openhcl_diag();
399            tasks.push(resources.driver.spawn("timer-watchdog", async move {
400                PolledTimer::new(&driver).sleep(TIMER_DURATION).await;
401                tracing::warn!("Test timeout reached after {TIMEOUT_DURATION_MINUTES} minutes, collecting diagnostics.");
402                let mut timeout_tasks = Vec::new();
403                if let Some(inspector) = vmm_inspector {
404                    timeout_tasks.push(inspect_task.clone()("vmm", &driver, Box::pin(async move { inspector.inspect_all().await })) );
405                }
406                if let Some(openhcl_diag_handler) = openhcl_diag_handler {
407                    timeout_tasks.push(inspect_task("openhcl", &driver, Box::pin(async move { openhcl_diag_handler.inspect("", None, None).await })));
408                }
409                futures::future::join_all(timeout_tasks).await;
410                tracing::error!("Test time out diagnostics collection complete, aborting.");
411                panic!("Test timed out");
412            }));
413        }
414
415        if let Some(mut framebuffer_access) = runtime.take_framebuffer_access() {
416            let mut timer = PolledTimer::new(&resources.driver);
417            let log_source = resources.log_source.clone();
418
419            tasks.push(
420                resources
421                    .driver
422                    .spawn("petri-watchdog-screenshot", async move {
423                        let mut image = Vec::new();
424                        let mut last_image = Vec::new();
425                        loop {
426                            timer.sleep(Duration::from_secs(2)).await;
427                            tracing::trace!("Taking screenshot.");
428
429                            let VmScreenshotMeta {
430                                color,
431                                width,
432                                height,
433                            } = match framebuffer_access.screenshot(&mut image).await {
434                                Ok(Some(meta)) => meta,
435                                Ok(None) => {
436                                    tracing::debug!("VM off, skipping screenshot.");
437                                    continue;
438                                }
439                                Err(e) => {
440                                    tracing::error!(?e, "Failed to take screenshot");
441                                    continue;
442                                }
443                            };
444
445                            if image == last_image {
446                                tracing::debug!("No change in framebuffer, skipping screenshot.");
447                                continue;
448                            }
449
450                            let r =
451                                log_source
452                                    .create_attachment("screenshot.png")
453                                    .and_then(|mut f| {
454                                        image::write_buffer_with_format(
455                                            &mut f,
456                                            &image,
457                                            width.into(),
458                                            height.into(),
459                                            color,
460                                            image::ImageFormat::Png,
461                                        )
462                                        .map_err(Into::into)
463                                    });
464
465                            if let Err(e) = r {
466                                tracing::error!(?e, "Failed to save screenshot");
467                            } else {
468                                tracing::info!("Screenshot saved.");
469                            }
470
471                            std::mem::swap(&mut image, &mut last_image);
472                        }
473                    }),
474            );
475        }
476
477        Ok(tasks)
478    }
479
480    /// Configure the test to expect a boot failure from the VM.
481    /// Useful for negative tests.
482    pub fn with_expect_boot_failure(mut self) -> Self {
483        self.expected_boot_event = Some(FirmwareEvent::BootFailed);
484        self
485    }
486
487    /// Configure the test to not expect any boot event.
488    /// Useful for tests that do not boot a VTL0 guest.
489    pub fn with_expect_no_boot_event(mut self) -> Self {
490        self.expected_boot_event = None;
491        self
492    }
493
494    /// Allow the VM to reset once at the beginning of the test. Should only be
495    /// used if you are using a special VM configuration that causes the guest
496    /// to reboot when it usually wouldn't.
497    pub fn with_expect_reset(mut self) -> Self {
498        self.override_expect_reset = true;
499        self
500    }
501
502    /// Set the VM to enable secure boot and inject the templates per OS flavor.
503    pub fn with_secure_boot(mut self) -> Self {
504        self.config
505            .firmware
506            .uefi_config_mut()
507            .expect("Secure boot is only supported for UEFI firmware.")
508            .secure_boot_enabled = true;
509
510        match self.os_flavor() {
511            OsFlavor::Windows => self.with_windows_secure_boot_template(),
512            OsFlavor::Linux => self.with_uefi_ca_secure_boot_template(),
513            _ => panic!(
514                "Secure boot unsupported for OS flavor {:?}",
515                self.os_flavor()
516            ),
517        }
518    }
519
520    /// Inject Windows secure boot templates into the VM's UEFI.
521    pub fn with_windows_secure_boot_template(mut self) -> Self {
522        self.config
523            .firmware
524            .uefi_config_mut()
525            .expect("Secure boot is only supported for UEFI firmware.")
526            .secure_boot_template = Some(SecureBootTemplate::MicrosoftWindows);
527        self
528    }
529
530    /// Inject UEFI CA secure boot templates into the VM's UEFI.
531    pub fn with_uefi_ca_secure_boot_template(mut self) -> Self {
532        self.config
533            .firmware
534            .uefi_config_mut()
535            .expect("Secure boot is only supported for UEFI firmware.")
536            .secure_boot_template = Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority);
537        self
538    }
539
540    /// Set the VM to use the specified processor topology.
541    pub fn with_processor_topology(mut self, topology: ProcessorTopology) -> Self {
542        self.config.proc_topology = topology;
543        self
544    }
545
546    /// Set the VM to use the specified processor topology.
547    pub fn with_memory(mut self, memory: MemoryConfig) -> Self {
548        self.config.memory = memory;
549        self
550    }
551
552    /// Sets a custom OpenHCL IGVM VTL2 address type. This controls the behavior
553    /// of where VTL2 is placed in address space, and also the total size of memory
554    /// allocated for VTL2. VTL2 start will fail if `address_type` is specified
555    /// and leads to the loader allocating less memory than what is in the IGVM file.
556    pub fn with_vtl2_base_address_type(mut self, address_type: Vtl2BaseAddressType) -> Self {
557        self.config
558            .firmware
559            .openhcl_config_mut()
560            .expect("OpenHCL firmware is required to set custom VTL2 address type.")
561            .vtl2_base_address_type = Some(address_type);
562        self
563    }
564
565    /// Sets a custom OpenHCL IGVM file to use.
566    pub fn with_custom_openhcl(mut self, artifact: ResolvedArtifact<impl IsOpenhclIgvm>) -> Self {
567        match &mut self.config.firmware {
568            Firmware::OpenhclLinuxDirect { igvm_path, .. }
569            | Firmware::OpenhclPcat { igvm_path, .. }
570            | Firmware::OpenhclUefi { igvm_path, .. } => {
571                *igvm_path = artifact.erase();
572            }
573            Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } | Firmware::Pcat { .. } => {
574                panic!("Custom OpenHCL is only supported for OpenHCL firmware.")
575            }
576        }
577        self
578    }
579
580    /// Sets the command line for the paravisor.
581    pub fn with_openhcl_command_line(mut self, additional_command_line: &str) -> Self {
582        append_cmdline(
583            &mut self
584                .config
585                .firmware
586                .openhcl_config_mut()
587                .expect("OpenHCL command line is only supported for OpenHCL firmware.")
588                .command_line,
589            additional_command_line,
590        );
591        self
592    }
593
594    /// Enable confidential filtering, even if the VM is not confidential.
595    pub fn with_confidential_filtering(self) -> Self {
596        if !self.config.firmware.is_openhcl() {
597            panic!("Confidential filtering is only supported for OpenHCL");
598        }
599        self.with_openhcl_command_line(&format!(
600            "{}=1 {}=0",
601            underhill_confidentiality::OPENHCL_CONFIDENTIAL_ENV_VAR_NAME,
602            underhill_confidentiality::OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME
603        ))
604    }
605
606    /// Sets the command line parameters passed to OpenHCL related to logging.
607    pub fn with_openhcl_log_levels(mut self, levels: OpenHclLogConfig) -> Self {
608        self.config
609            .firmware
610            .openhcl_config_mut()
611            .expect("OpenHCL firmware is required to set custom OpenHCL log levels.")
612            .log_levels = levels;
613        self
614    }
615
616    /// Adds a file to the VM's pipette agent image.
617    pub fn with_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
618        self.config
619            .agent_image
620            .as_mut()
621            .expect("no guest pipette")
622            .add_file(name, artifact);
623        self
624    }
625
626    /// Adds a file to the paravisor's pipette agent image.
627    pub fn with_openhcl_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
628        self.config
629            .openhcl_agent_image
630            .as_mut()
631            .expect("no openhcl pipette")
632            .add_file(name, artifact);
633        self
634    }
635
636    /// Sets whether UEFI frontpage is enabled.
637    pub fn with_uefi_frontpage(mut self, enable: bool) -> Self {
638        self.config
639            .firmware
640            .uefi_config_mut()
641            .expect("UEFI frontpage is only supported for UEFI firmware.")
642            .disable_frontpage = !enable;
643        self
644    }
645
646    /// Sets whether UEFI should always attempt a default boot.
647    pub fn with_default_boot_always_attempt(mut self, enable: bool) -> Self {
648        self.config
649            .firmware
650            .uefi_config_mut()
651            .expect("Default boot always attempt is only supported for UEFI firmware.")
652            .default_boot_always_attempt = enable;
653        self
654    }
655
656    /// Run the VM with Enable VMBus relay enabled
657    pub fn with_vmbus_redirect(mut self, enable: bool) -> Self {
658        self.config
659            .firmware
660            .openhcl_config_mut()
661            .expect("VMBus redirection is only supported for OpenHCL firmware.")
662            .vmbus_redirect = enable;
663        self
664    }
665
666    /// Specify the guest state lifetime for the VM
667    pub fn with_guest_state_lifetime(
668        mut self,
669        guest_state_lifetime: PetriGuestStateLifetime,
670    ) -> Self {
671        let disk = match self.config.vmgs {
672            PetriVmgsResource::Disk(disk)
673            | PetriVmgsResource::ReprovisionOnFailure(disk)
674            | PetriVmgsResource::Reprovision(disk) => disk,
675            PetriVmgsResource::Ephemeral => PetriVmgsDisk::default(),
676        };
677        self.config.vmgs = match guest_state_lifetime {
678            PetriGuestStateLifetime::Disk => PetriVmgsResource::Disk(disk),
679            PetriGuestStateLifetime::ReprovisionOnFailure => {
680                PetriVmgsResource::ReprovisionOnFailure(disk)
681            }
682            PetriGuestStateLifetime::Reprovision => PetriVmgsResource::Reprovision(disk),
683            PetriGuestStateLifetime::Ephemeral => {
684                if !matches!(disk.disk, PetriDiskType::Memory) {
685                    panic!("attempted to use ephemeral guest state after specifying backing vmgs")
686                }
687                PetriVmgsResource::Ephemeral
688            }
689        };
690        self
691    }
692
693    /// Specify the guest state encryption policy for the VM
694    pub fn with_guest_state_encryption(mut self, policy: GuestStateEncryptionPolicy) -> Self {
695        match &mut self.config.vmgs {
696            PetriVmgsResource::Disk(vmgs)
697            | PetriVmgsResource::ReprovisionOnFailure(vmgs)
698            | PetriVmgsResource::Reprovision(vmgs) => {
699                vmgs.encryption_policy = policy;
700            }
701            PetriVmgsResource::Ephemeral => {
702                panic!("attempted to encrypt ephemeral guest state")
703            }
704        }
705        self
706    }
707
708    /// Use the specified backing VMGS file
709    pub fn with_initial_vmgs(self, disk: ResolvedArtifact<impl IsTestVmgs>) -> Self {
710        self.with_backing_vmgs(PetriDiskType::Differencing(disk.into()))
711    }
712
713    /// Use the specified backing VMGS file
714    pub fn with_persistent_vmgs(self, disk: impl AsRef<Path>) -> Self {
715        self.with_backing_vmgs(PetriDiskType::Persistent(disk.as_ref().to_path_buf()))
716    }
717
718    fn with_backing_vmgs(mut self, disk: PetriDiskType) -> Self {
719        match &mut self.config.vmgs {
720            PetriVmgsResource::Disk(vmgs)
721            | PetriVmgsResource::ReprovisionOnFailure(vmgs)
722            | PetriVmgsResource::Reprovision(vmgs) => {
723                if !matches!(vmgs.disk, PetriDiskType::Memory) {
724                    panic!("already specified a backing vmgs file");
725                }
726                vmgs.disk = disk;
727            }
728            PetriVmgsResource::Ephemeral => {
729                panic!("attempted to specify a backing vmgs with ephemeral guest state")
730            }
731        }
732        self
733    }
734
735    /// Set the boot device type for the VM.
736    ///
737    /// This overrides the default, which is determined by the firmware type.
738    pub fn with_boot_device_type(mut self, boot: BootDeviceType) -> Self {
739        self.config.boot_device_type = boot;
740        self
741    }
742
743    /// Enable or disable the TPM state persistence for the VM.
744    pub fn with_tpm_state_persistence(mut self, tpm_state_persistence: bool) -> Self {
745        self.config.tpm_state_persistence = tpm_state_persistence;
746        self
747    }
748
749    /// Get VM's guest OS flavor
750    pub fn os_flavor(&self) -> OsFlavor {
751        self.config.firmware.os_flavor()
752    }
753
754    /// Get whether the VM will use OpenHCL
755    pub fn is_openhcl(&self) -> bool {
756        self.config.firmware.is_openhcl()
757    }
758
759    /// Get the isolation type of the VM
760    pub fn isolation(&self) -> Option<IsolationType> {
761        self.config.firmware.isolation()
762    }
763
764    /// Get the machine architecture
765    pub fn arch(&self) -> MachineArch {
766        self.config.arch
767    }
768
769    /// Get the default OpenHCL servicing flags for this config
770    pub fn default_servicing_flags(&self) -> OpenHclServicingFlags {
771        T::default_servicing_flags()
772    }
773
774    /// Get the backend-specific config builder
775    pub fn modify_backend(
776        mut self,
777        f: impl FnOnce(T::VmmConfig) -> T::VmmConfig + 'static + Send,
778    ) -> Self {
779        if self.modify_vmm_config.is_some() {
780            panic!("only one modify_backend allowed");
781        }
782        self.modify_vmm_config = Some(Box::new(f));
783        self
784    }
785}
786
787impl<T: PetriVmmBackend> PetriVm<T> {
788    /// Immediately tear down the VM.
789    pub async fn teardown(self) -> anyhow::Result<()> {
790        tracing::info!("Tearing down VM...");
791        self.runtime.teardown().await
792    }
793
794    /// Wait for the VM to halt, returning the reason for the halt.
795    pub async fn wait_for_halt(&mut self) -> anyhow::Result<PetriHaltReason> {
796        tracing::info!("Waiting for VM to halt...");
797        let halt_reason = self.runtime.wait_for_halt(false).await?;
798        tracing::info!("VM halted: {halt_reason:?}. Cancelling watchdogs...");
799        futures::future::join_all(self.watchdog_tasks.drain(..).map(|t| t.cancel())).await;
800        Ok(halt_reason)
801    }
802
803    /// Wait for the VM to cleanly shutdown.
804    pub async fn wait_for_clean_shutdown(&mut self) -> anyhow::Result<()> {
805        let halt_reason = self.wait_for_halt().await?;
806        if halt_reason != PetriHaltReason::PowerOff {
807            anyhow::bail!("Expected PowerOff, got {halt_reason:?}");
808        }
809        tracing::info!("VM was cleanly powered off and torn down.");
810        Ok(())
811    }
812
813    /// Wait for the VM to halt, returning the reason for the halt,
814    /// and tear down the VM.
815    pub async fn wait_for_teardown(mut self) -> anyhow::Result<PetriHaltReason> {
816        let halt_reason = self.wait_for_halt().await?;
817        self.teardown().await?;
818        Ok(halt_reason)
819    }
820
821    /// Wait for the VM to cleanly shutdown and tear down the VM.
822    pub async fn wait_for_clean_teardown(mut self) -> anyhow::Result<()> {
823        self.wait_for_clean_shutdown().await?;
824        self.teardown().await
825    }
826
827    /// Wait for the VM to reset. Does not wait for pipette.
828    pub async fn wait_for_reset_no_agent(&mut self) -> anyhow::Result<()> {
829        self.wait_for_reset_core().await?;
830        self.wait_for_expected_boot_event().await?;
831        Ok(())
832    }
833
834    /// Wait for the VM to reset and pipette to connect.
835    pub async fn wait_for_reset(&mut self) -> anyhow::Result<PipetteClient> {
836        self.wait_for_reset_no_agent().await?;
837        self.wait_for_agent().await
838    }
839
840    async fn wait_for_reset_core(&mut self) -> anyhow::Result<()> {
841        tracing::info!("Waiting for VM to reset...");
842        let halt_reason = self.runtime.wait_for_halt(true).await?;
843        if halt_reason != PetriHaltReason::Reset {
844            anyhow::bail!("Expected reset, got {halt_reason:?}");
845        }
846        tracing::info!("VM reset.");
847        Ok(())
848    }
849
850    /// Invoke Inspect on the running OpenHCL instance.
851    ///
852    /// IMPORTANT: As mentioned in the Guide, inspect output is *not* guaranteed
853    /// to be stable. Use this to test that components in OpenHCL are working as
854    /// you would expect. But, if you are adding a test simply to verify that
855    /// the inspect output as some other tool depends on it, then that is
856    /// incorrect.
857    ///
858    /// - `timeout` is enforced on the client side
859    /// - `path` and `depth` are passed to the [`inspect::Inspect`] machinery.
860    pub async fn inspect_openhcl(
861        &self,
862        path: impl Into<String>,
863        depth: Option<usize>,
864        timeout: Option<Duration>,
865    ) -> anyhow::Result<inspect::Node> {
866        self.openhcl_diag()?
867            .inspect(path.into().as_str(), depth, timeout)
868            .await
869    }
870
871    /// Test that we are able to inspect OpenHCL.
872    pub async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()> {
873        self.inspect_openhcl("", None, None).await.map(|_| ())
874    }
875
876    /// Wait for VTL 2 to report that it is ready to respond to commands.
877    /// Will fail if the VM is not running OpenHCL.
878    ///
879    /// This should only be necessary if you're doing something manual. All
880    /// Petri-provided methods will wait for VTL 2 to be ready automatically.
881    pub async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()> {
882        self.openhcl_diag()?.wait_for_vtl2().await
883    }
884
885    /// Get the kmsg stream from OpenHCL.
886    pub async fn kmsg(&self) -> anyhow::Result<diag_client::kmsg_stream::KmsgStream> {
887        self.openhcl_diag()?.kmsg().await
888    }
889
890    /// Gets a live core dump of the OpenHCL process specified by 'name' and
891    /// writes it to 'path'
892    pub async fn openhcl_core_dump(&self, name: &str, path: &Path) -> anyhow::Result<()> {
893        self.openhcl_diag()?.core_dump(name, path).await
894    }
895
896    /// Crashes the specified openhcl process
897    pub async fn openhcl_crash(&self, name: &str) -> anyhow::Result<()> {
898        self.openhcl_diag()?.crash(name).await
899    }
900
901    /// Wait for a connection from a pipette agent running in the guest.
902    /// Useful if you've rebooted the vm or are otherwise expecting a fresh connection.
903    async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient> {
904        self.runtime.wait_for_agent(false).await
905    }
906
907    /// Wait for a connection from a pipette agent running in VTL 2.
908    /// Useful if you've reset VTL 2 or are otherwise expecting a fresh connection.
909    /// Will fail if the VM is not running OpenHCL.
910    pub async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient> {
911        // VTL 2's pipette doesn't auto launch, only launch it on demand
912        self.launch_vtl2_pipette().await?;
913        self.runtime.wait_for_agent(true).await
914    }
915
916    /// Waits for an event emitted by the firmware about its boot status, and
917    /// verifies that it is the expected success value.
918    ///
919    /// * Linux Direct guests do not emit a boot event, so this method immediately returns Ok.
920    /// * PCAT guests may not emit an event depending on the PCAT version, this
921    ///   method is best effort for them.
922    async fn wait_for_expected_boot_event(&mut self) -> anyhow::Result<()> {
923        if let Some(expected_event) = self.expected_boot_event {
924            let event = self.wait_for_boot_event().await?;
925
926            anyhow::ensure!(
927                event == expected_event,
928                "Did not receive expected boot event"
929            );
930        } else {
931            tracing::warn!("Boot event not emitted for configured firmware or manually ignored.");
932        }
933
934        Ok(())
935    }
936
937    /// Waits for an event emitted by the firmware about its boot status, and
938    /// returns that status.
939    async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
940        tracing::info!("Waiting for boot event...");
941        let boot_event = loop {
942            match CancelContext::new()
943                .with_timeout(self.vmm_quirks.flaky_boot.unwrap_or(Duration::MAX))
944                .until_cancelled(self.runtime.wait_for_boot_event())
945                .await
946            {
947                Ok(res) => break res?,
948                Err(_) => {
949                    tracing::error!("Did not get boot event in required time, resetting...");
950                    if let Some(inspector) = self.runtime.inspector() {
951                        save_inspect(
952                            "vmm",
953                            Box::pin(async move { inspector.inspect_all().await }),
954                            &self.resources.log_source,
955                        )
956                        .await;
957                    }
958
959                    self.runtime.reset().await?;
960                    continue;
961                }
962            }
963        };
964        tracing::info!("Got boot event: {boot_event:?}");
965        Ok(boot_event)
966    }
967
968    /// Wait for the Hyper-V shutdown IC to be ready and use it to instruct
969    /// the guest to shutdown.
970    pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
971        tracing::info!("Waiting for enlightened shutdown to be ready");
972        self.runtime.wait_for_enlightened_shutdown_ready().await?;
973
974        // all guests used in testing have been observed to intermittently
975        // drop shutdown requests if they are sent too soon after the shutdown
976        // ic comes online. give them a little extra time.
977        // TODO: use a different method of determining whether the VM has booted
978        // or debug and fix the shutdown IC.
979        let mut wait_time = Duration::from_secs(10);
980
981        // some guests need even more time
982        if let Some(duration) = self.guest_quirks.hyperv_shutdown_ic_sleep {
983            wait_time += duration;
984        }
985
986        tracing::info!(
987            "Shutdown IC reported ready, waiting for an extra {}s",
988            wait_time.as_secs()
989        );
990        PolledTimer::new(&self.resources.driver)
991            .sleep(wait_time)
992            .await;
993
994        tracing::info!("Sending enlightened shutdown command");
995        self.runtime.send_enlightened_shutdown(kind).await
996    }
997
998    /// Instruct the OpenHCL to restart the VTL2 paravisor. Will fail if the VM
999    /// is not running OpenHCL. Will also fail if the VM is not running.
1000    pub async fn restart_openhcl(
1001        &mut self,
1002        new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1003        flags: OpenHclServicingFlags,
1004    ) -> anyhow::Result<()> {
1005        self.runtime
1006            .restart_openhcl(&new_openhcl.erase(), flags)
1007            .await
1008    }
1009
1010    /// Instruct the OpenHCL to save the state of the VTL2 paravisor. Will fail if the VM
1011    /// is not running OpenHCL. Will also fail if the VM is not running or if this is called twice in succession
1012    pub async fn save_openhcl(
1013        &mut self,
1014        new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1015        flags: OpenHclServicingFlags,
1016    ) -> anyhow::Result<()> {
1017        self.runtime.save_openhcl(&new_openhcl.erase(), flags).await
1018    }
1019
1020    /// Instruct the OpenHCL to restore the state of the VTL2 paravisor. Will fail if the VM
1021    /// is not running OpenHCL. Will also fail if the VM is running or if this is called without prior save
1022    pub async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
1023        self.runtime.restore_openhcl().await
1024    }
1025
1026    /// Get VM's guest OS flavor
1027    pub fn arch(&self) -> MachineArch {
1028        self.arch
1029    }
1030
1031    /// Get the inner runtime backend to make backend-specific calls
1032    pub fn backend(&mut self) -> &mut T::VmRuntime {
1033        &mut self.runtime
1034    }
1035
1036    async fn launch_vtl2_pipette(&self) -> anyhow::Result<()> {
1037        tracing::debug!("Launching VTL 2 pipette...");
1038
1039        // Start pipette through DiagClient
1040        let res = self
1041            .openhcl_diag()?
1042            .run_vtl2_command("sh", &["-c", "mkdir /cidata && mount LABEL=cidata /cidata"])
1043            .await?;
1044
1045        if !res.exit_status.success() {
1046            anyhow::bail!("Failed to mount VTL 2 pipette drive: {:?}", res);
1047        }
1048
1049        let res = self
1050            .openhcl_diag()?
1051            .run_detached_vtl2_command("sh", &["-c", "/cidata/pipette 2>&1 | logger &"])
1052            .await?;
1053
1054        if !res.success() {
1055            anyhow::bail!("Failed to spawn VTL 2 pipette: {:?}", res);
1056        }
1057
1058        Ok(())
1059    }
1060
1061    fn openhcl_diag(&self) -> anyhow::Result<&OpenHclDiagHandler> {
1062        if let Some(ohd) = self.openhcl_diag_handler.as_ref() {
1063            Ok(ohd)
1064        } else {
1065            anyhow::bail!("VM is not configured with OpenHCL")
1066        }
1067    }
1068
1069    /// Get the path to the VM's guest state file
1070    pub async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
1071        self.runtime.get_guest_state_file().await
1072    }
1073}
1074
1075/// A running VM that tests can interact with.
1076#[async_trait]
1077pub trait PetriVmRuntime: Send + Sync + 'static {
1078    /// Interface for inspecting the VM
1079    type VmInspector: PetriVmInspector;
1080    /// Interface for accessing the framebuffer
1081    type VmFramebufferAccess: PetriVmFramebufferAccess;
1082
1083    /// Cleanly tear down the VM immediately.
1084    async fn teardown(self) -> anyhow::Result<()>;
1085    /// Wait for the VM to halt, returning the reason for the halt. The VM
1086    /// should automatically restart the VM on reset if `allow_reset` is true.
1087    async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReason>;
1088    /// Wait for a connection from a pipette agent
1089    async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>;
1090    /// Get an OpenHCL diagnostics handler for the VM
1091    fn openhcl_diag(&self) -> Option<OpenHclDiagHandler>;
1092    /// Waits for an event emitted by the firmware about its boot status, and
1093    /// returns that status.
1094    async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent>;
1095    /// Waits for the Hyper-V shutdown IC to be ready
1096    // TODO: return a receiver that will be closed when it is no longer ready.
1097    async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()>;
1098    /// Instruct the guest to shutdown via the Hyper-V shutdown IC.
1099    async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
1100    /// Instruct the OpenHCL to restart the VTL2 paravisor. Will fail if the VM
1101    /// is not running OpenHCL. Will also fail if the VM is not running.
1102    async fn restart_openhcl(
1103        &mut self,
1104        new_openhcl: &ResolvedArtifact,
1105        flags: OpenHclServicingFlags,
1106    ) -> anyhow::Result<()>;
1107    /// Instruct the OpenHCL to save the state of the VTL2 paravisor. Will fail if the VM
1108    /// is not running OpenHCL. Will also fail if the VM is not running or if this is called twice in succession
1109    /// without a call to `restore_openhcl`.
1110    async fn save_openhcl(
1111        &mut self,
1112        new_openhcl: &ResolvedArtifact,
1113        flags: OpenHclServicingFlags,
1114    ) -> anyhow::Result<()>;
1115    /// Instruct the OpenHCL to restore the state of the VTL2 paravisor. Will fail if the VM
1116    /// is not running OpenHCL. Will also fail if the VM is running or if this is called without prior save.
1117    async fn restore_openhcl(&mut self) -> anyhow::Result<()>;
1118    /// If the backend supports it, get an inspect interface
1119    fn inspector(&self) -> Option<Self::VmInspector> {
1120        None
1121    }
1122    /// If the backend supports it, take the screenshot interface
1123    /// (subsequent calls may return None).
1124    fn take_framebuffer_access(&mut self) -> Option<Self::VmFramebufferAccess> {
1125        None
1126    }
1127    /// Issue a hard reset to the VM
1128    async fn reset(&mut self) -> anyhow::Result<()>;
1129    /// Get the path to the VM's guest state file
1130    async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
1131        Ok(None)
1132    }
1133}
1134
1135/// Interface for getting information about the state of the VM
1136#[async_trait]
1137pub trait PetriVmInspector: Send + Sync + 'static {
1138    /// Get information about the state of the VM
1139    async fn inspect_all(&self) -> anyhow::Result<inspect::Node>;
1140}
1141
1142/// Use this for the associated type if not supported
1143pub struct NoPetriVmInspector;
1144#[async_trait]
1145impl PetriVmInspector for NoPetriVmInspector {
1146    async fn inspect_all(&self) -> anyhow::Result<inspect::Node> {
1147        unreachable!()
1148    }
1149}
1150
1151/// Raw VM screenshot
1152pub struct VmScreenshotMeta {
1153    /// color encoding used by the image
1154    pub color: image::ExtendedColorType,
1155    /// x dimension
1156    pub width: u16,
1157    /// y dimension
1158    pub height: u16,
1159}
1160
1161/// Interface for getting screenshots of the VM
1162#[async_trait]
1163pub trait PetriVmFramebufferAccess: Send + 'static {
1164    /// Populates the provided buffer with a screenshot of the VM,
1165    /// returning the dimensions and color type.
1166    async fn screenshot(&mut self, image: &mut Vec<u8>)
1167    -> anyhow::Result<Option<VmScreenshotMeta>>;
1168}
1169
1170/// Use this for the associated type if not supported
1171pub struct NoPetriVmFramebufferAccess;
1172#[async_trait]
1173impl PetriVmFramebufferAccess for NoPetriVmFramebufferAccess {
1174    async fn screenshot(
1175        &mut self,
1176        _image: &mut Vec<u8>,
1177    ) -> anyhow::Result<Option<VmScreenshotMeta>> {
1178        unreachable!()
1179    }
1180}
1181
1182/// Common processor topology information for the VM.
1183#[derive(Debug)]
1184pub struct ProcessorTopology {
1185    /// The number of virtual processors.
1186    pub vp_count: u32,
1187    /// Whether SMT (hyperthreading) is enabled.
1188    pub enable_smt: Option<bool>,
1189    /// The number of virtual processors per socket.
1190    pub vps_per_socket: Option<u32>,
1191    /// The APIC configuration (x86-64 only).
1192    pub apic_mode: Option<ApicMode>,
1193}
1194
1195impl Default for ProcessorTopology {
1196    fn default() -> Self {
1197        Self {
1198            vp_count: 2,
1199            enable_smt: None,
1200            vps_per_socket: None,
1201            apic_mode: None,
1202        }
1203    }
1204}
1205
1206/// The APIC mode for the VM.
1207#[derive(Debug, Clone, Copy)]
1208pub enum ApicMode {
1209    /// xAPIC mode only.
1210    Xapic,
1211    /// x2APIC mode supported but not enabled at boot.
1212    X2apicSupported,
1213    /// x2APIC mode enabled at boot.
1214    X2apicEnabled,
1215}
1216
1217/// Common memory configuration information for the VM.
1218pub struct MemoryConfig {
1219    /// Specifies the amount of memory, in bytes, to assign to the
1220    /// virtual machine.
1221    pub startup_bytes: u64,
1222    /// Specifies the minimum and maximum amount of dynamic memory, in bytes.
1223    ///
1224    /// Dynamic memory will be disabled if this is `None`.
1225    pub dynamic_memory_range: Option<(u64, u64)>,
1226}
1227
1228impl Default for MemoryConfig {
1229    fn default() -> Self {
1230        Self {
1231            startup_bytes: 0x1_0000_0000,
1232            dynamic_memory_range: None,
1233        }
1234    }
1235}
1236
1237/// UEFI firmware configuration
1238#[derive(Debug)]
1239pub struct UefiConfig {
1240    /// Enable secure boot
1241    pub secure_boot_enabled: bool,
1242    /// Secure boot template
1243    pub secure_boot_template: Option<SecureBootTemplate>,
1244    /// Disable the UEFI frontpage which will cause the VM to shutdown instead when unable to boot.
1245    pub disable_frontpage: bool,
1246    /// Always attempt a default boot
1247    pub default_boot_always_attempt: bool,
1248}
1249
1250impl Default for UefiConfig {
1251    fn default() -> Self {
1252        Self {
1253            secure_boot_enabled: false,
1254            secure_boot_template: None,
1255            disable_frontpage: true,
1256            default_boot_always_attempt: false,
1257        }
1258    }
1259}
1260
1261/// Control the logging configuration of OpenHCL for this VM.
1262#[derive(Debug, Clone)]
1263pub enum OpenHclLogConfig {
1264    /// Use the default log levels used by petri tests. This will forward
1265    /// `OPENVMM_LOG` and `OPENVMM_SHOW_SPANS` from the environment if they are
1266    /// set, otherwise it will use `debug` and `true` respectively
1267    TestDefault,
1268    /// Use the built-in default log levels of OpenHCL (e.g. don't pass
1269    /// OPENVMM_LOG or OPENVMM_SHOW_SPANS)
1270    BuiltInDefault,
1271    /// Use the provided custom log levels (e.g.
1272    /// `OPENVMM_LOG=info,disk_nvme=debug OPENVMM_SHOW_SPANS=true`)
1273    Custom(String),
1274}
1275
1276/// OpenHCL configuration
1277#[derive(Debug, Clone)]
1278pub struct OpenHclConfig {
1279    /// Emulate SCSI via NVME to VTL2, with the provided namespace ID on
1280    /// the controller with `BOOT_NVME_INSTANCE`.
1281    pub vtl2_nvme_boot: bool,
1282    /// Whether to enable VMBus redirection
1283    pub vmbus_redirect: bool,
1284    /// Test-specified command-line parameters to pass to OpenHCL. VM backends
1285    /// should use [`OpenHclConfig::command_line()`] rather than reading this
1286    /// directly.
1287    pub command_line: Option<String>,
1288    /// Command line parameters that control OpenHCL logging behavior. Separate
1289    /// from `command_line` so that petri can decide to use default log
1290    /// levels.
1291    pub log_levels: OpenHclLogConfig,
1292    /// How to place VTL2 in address space. If `None`, the backend VMM
1293    /// will decide on default behavior.
1294    pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
1295}
1296
1297impl OpenHclConfig {
1298    /// Returns the command line to pass to OpenHCL based on these parameters. Aggregates
1299    /// the command line and log levels.
1300    pub fn command_line(&self) -> String {
1301        let mut cmdline = self.command_line.clone();
1302        match &self.log_levels {
1303            OpenHclLogConfig::TestDefault => {
1304                let default_log_levels = {
1305                    // Forward OPENVMM_LOG and OPENVMM_SHOW_SPANS to OpenHCL if they're set.
1306                    let openhcl_tracing = if let Ok(x) =
1307                        std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG"))
1308                    {
1309                        format!("OPENVMM_LOG={x}")
1310                    } else {
1311                        "OPENVMM_LOG=debug".to_owned()
1312                    };
1313                    let openhcl_show_spans = if let Ok(x) = std::env::var("OPENVMM_SHOW_SPANS") {
1314                        format!("OPENVMM_SHOW_SPANS={x}")
1315                    } else {
1316                        "OPENVMM_SHOW_SPANS=true".to_owned()
1317                    };
1318                    format!("{openhcl_tracing} {openhcl_show_spans}")
1319                };
1320                append_cmdline(&mut cmdline, &default_log_levels);
1321            }
1322            OpenHclLogConfig::BuiltInDefault => {
1323                // do nothing, use whatever the built-in default is
1324            }
1325            OpenHclLogConfig::Custom(levels) => {
1326                append_cmdline(&mut cmdline, levels);
1327            }
1328        }
1329
1330        cmdline.unwrap_or_default()
1331    }
1332}
1333
1334impl Default for OpenHclConfig {
1335    fn default() -> Self {
1336        Self {
1337            vtl2_nvme_boot: false,
1338            vmbus_redirect: false,
1339            command_line: None,
1340            log_levels: OpenHclLogConfig::TestDefault,
1341            vtl2_base_address_type: None,
1342        }
1343    }
1344}
1345
1346/// Firmware to load into the test VM.
1347#[derive(Debug)]
1348pub enum Firmware {
1349    /// Boot Linux directly, without any firmware.
1350    LinuxDirect {
1351        /// The kernel to boot.
1352        kernel: ResolvedArtifact,
1353        /// The initrd to use.
1354        initrd: ResolvedArtifact,
1355    },
1356    /// Boot Linux directly, without any firmware, with OpenHCL in VTL2.
1357    OpenhclLinuxDirect {
1358        /// The path to the IGVM file to use.
1359        igvm_path: ResolvedArtifact,
1360        /// OpenHCL configuration
1361        openhcl_config: OpenHclConfig,
1362    },
1363    /// Boot a PCAT-based VM.
1364    Pcat {
1365        /// The guest OS the VM will boot into.
1366        guest: PcatGuest,
1367        /// The firmware to use.
1368        bios_firmware: ResolvedOptionalArtifact,
1369        /// The SVGA firmware to use.
1370        svga_firmware: ResolvedOptionalArtifact,
1371    },
1372    /// Boot a PCAT-based VM with OpenHCL in VTL2.
1373    OpenhclPcat {
1374        /// The guest OS the VM will boot into.
1375        guest: PcatGuest,
1376        /// The path to the IGVM file to use.
1377        igvm_path: ResolvedArtifact,
1378        /// The firmware to use.
1379        bios_firmware: ResolvedOptionalArtifact,
1380        /// The SVGA firmware to use.
1381        svga_firmware: ResolvedOptionalArtifact,
1382        /// OpenHCL configuration
1383        openhcl_config: OpenHclConfig,
1384    },
1385    /// Boot a UEFI-based VM.
1386    Uefi {
1387        /// The guest OS the VM will boot into.
1388        guest: UefiGuest,
1389        /// The firmware to use.
1390        uefi_firmware: ResolvedArtifact,
1391        /// UEFI configuration
1392        uefi_config: UefiConfig,
1393    },
1394    /// Boot a UEFI-based VM with OpenHCL in VTL2.
1395    OpenhclUefi {
1396        /// The guest OS the VM will boot into.
1397        guest: UefiGuest,
1398        /// The isolation type of the VM.
1399        isolation: Option<IsolationType>,
1400        /// The path to the IGVM file to use.
1401        igvm_path: ResolvedArtifact,
1402        /// UEFI configuration
1403        uefi_config: UefiConfig,
1404        /// OpenHCL configuration
1405        openhcl_config: OpenHclConfig,
1406    },
1407}
1408
1409/// The boot device type.
1410#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1411pub enum BootDeviceType {
1412    /// Don't initialize a boot device.
1413    None,
1414    /// Boot from IDE.
1415    Ide,
1416    /// Boot from SCSI.
1417    Scsi,
1418    /// Boot from an NVMe controller.
1419    Nvme,
1420}
1421
1422impl Firmware {
1423    /// Constructs a standard [`Firmware::LinuxDirect`] configuration.
1424    pub fn linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
1425        use petri_artifacts_vmm_test::artifacts::loadable::*;
1426        match arch {
1427            MachineArch::X86_64 => Firmware::LinuxDirect {
1428                kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_X64).erase(),
1429                initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
1430            },
1431            MachineArch::Aarch64 => Firmware::LinuxDirect {
1432                kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_AARCH64).erase(),
1433                initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_AARCH64).erase(),
1434            },
1435        }
1436    }
1437
1438    /// Constructs a standard [`Firmware::OpenhclLinuxDirect`] configuration.
1439    pub fn openhcl_linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
1440        use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
1441        match arch {
1442            MachineArch::X86_64 => Firmware::OpenhclLinuxDirect {
1443                igvm_path: resolver.require(LATEST_LINUX_DIRECT_TEST_X64).erase(),
1444                openhcl_config: Default::default(),
1445            },
1446            MachineArch::Aarch64 => todo!("Linux direct not yet supported on aarch64"),
1447        }
1448    }
1449
1450    /// Constructs a standard [`Firmware::Pcat`] configuration.
1451    pub fn pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
1452        use petri_artifacts_vmm_test::artifacts::loadable::*;
1453        Firmware::Pcat {
1454            guest,
1455            bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
1456            svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
1457        }
1458    }
1459
1460    /// Constructs a standard [`Firmware::Uefi`] configuration.
1461    pub fn uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch, guest: UefiGuest) -> Self {
1462        use petri_artifacts_vmm_test::artifacts::loadable::*;
1463        let uefi_firmware = match arch {
1464            MachineArch::X86_64 => resolver.require(UEFI_FIRMWARE_X64).erase(),
1465            MachineArch::Aarch64 => resolver.require(UEFI_FIRMWARE_AARCH64).erase(),
1466        };
1467        Firmware::Uefi {
1468            guest,
1469            uefi_firmware,
1470            uefi_config: Default::default(),
1471        }
1472    }
1473
1474    /// Constructs a standard [`Firmware::OpenhclUefi`] configuration.
1475    pub fn openhcl_uefi(
1476        resolver: &ArtifactResolver<'_>,
1477        arch: MachineArch,
1478        guest: UefiGuest,
1479        isolation: Option<IsolationType>,
1480        vtl2_nvme_boot: bool,
1481    ) -> Self {
1482        use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
1483        let igvm_path = match arch {
1484            MachineArch::X86_64 if isolation.is_some() => resolver.require(LATEST_CVM_X64).erase(),
1485            MachineArch::X86_64 => resolver.require(LATEST_STANDARD_X64).erase(),
1486            MachineArch::Aarch64 => resolver.require(LATEST_STANDARD_AARCH64).erase(),
1487        };
1488        Firmware::OpenhclUefi {
1489            guest,
1490            isolation,
1491            igvm_path,
1492            uefi_config: Default::default(),
1493            openhcl_config: OpenHclConfig {
1494                vtl2_nvme_boot,
1495                ..Default::default()
1496            },
1497        }
1498    }
1499
1500    fn is_openhcl(&self) -> bool {
1501        match self {
1502            Firmware::OpenhclLinuxDirect { .. }
1503            | Firmware::OpenhclUefi { .. }
1504            | Firmware::OpenhclPcat { .. } => true,
1505            Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => false,
1506        }
1507    }
1508
1509    fn isolation(&self) -> Option<IsolationType> {
1510        match self {
1511            Firmware::OpenhclUefi { isolation, .. } => *isolation,
1512            Firmware::LinuxDirect { .. }
1513            | Firmware::Pcat { .. }
1514            | Firmware::Uefi { .. }
1515            | Firmware::OpenhclLinuxDirect { .. }
1516            | Firmware::OpenhclPcat { .. } => None,
1517        }
1518    }
1519
1520    fn is_linux_direct(&self) -> bool {
1521        match self {
1522            Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => true,
1523            Firmware::Pcat { .. }
1524            | Firmware::Uefi { .. }
1525            | Firmware::OpenhclUefi { .. }
1526            | Firmware::OpenhclPcat { .. } => false,
1527        }
1528    }
1529
1530    fn is_pcat(&self) -> bool {
1531        match self {
1532            Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => true,
1533            Firmware::Uefi { .. }
1534            | Firmware::OpenhclUefi { .. }
1535            | Firmware::LinuxDirect { .. }
1536            | Firmware::OpenhclLinuxDirect { .. } => false,
1537        }
1538    }
1539
1540    fn os_flavor(&self) -> OsFlavor {
1541        match self {
1542            Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => OsFlavor::Linux,
1543            Firmware::Uefi {
1544                guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
1545                ..
1546            }
1547            | Firmware::OpenhclUefi {
1548                guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
1549                ..
1550            } => OsFlavor::Uefi,
1551            Firmware::Pcat {
1552                guest: PcatGuest::Vhd(cfg),
1553                ..
1554            }
1555            | Firmware::OpenhclPcat {
1556                guest: PcatGuest::Vhd(cfg),
1557                ..
1558            }
1559            | Firmware::Uefi {
1560                guest: UefiGuest::Vhd(cfg),
1561                ..
1562            }
1563            | Firmware::OpenhclUefi {
1564                guest: UefiGuest::Vhd(cfg),
1565                ..
1566            } => cfg.os_flavor,
1567            Firmware::Pcat {
1568                guest: PcatGuest::Iso(cfg),
1569                ..
1570            }
1571            | Firmware::OpenhclPcat {
1572                guest: PcatGuest::Iso(cfg),
1573                ..
1574            } => cfg.os_flavor,
1575        }
1576    }
1577
1578    fn quirks(&self) -> GuestQuirks {
1579        match self {
1580            Firmware::Pcat {
1581                guest: PcatGuest::Vhd(cfg),
1582                ..
1583            }
1584            | Firmware::Uefi {
1585                guest: UefiGuest::Vhd(cfg),
1586                ..
1587            }
1588            | Firmware::OpenhclUefi {
1589                guest: UefiGuest::Vhd(cfg),
1590                ..
1591            } => cfg.quirks.clone(),
1592            Firmware::Pcat {
1593                guest: PcatGuest::Iso(cfg),
1594                ..
1595            } => cfg.quirks.clone(),
1596            _ => Default::default(),
1597        }
1598    }
1599
1600    fn expected_boot_event(&self) -> Option<FirmwareEvent> {
1601        match self {
1602            Firmware::LinuxDirect { .. }
1603            | Firmware::OpenhclLinuxDirect { .. }
1604            | Firmware::Uefi {
1605                guest: UefiGuest::GuestTestUefi(_),
1606                ..
1607            }
1608            | Firmware::OpenhclUefi {
1609                guest: UefiGuest::GuestTestUefi(_),
1610                ..
1611            } => None,
1612            Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => {
1613                // TODO: Handle older PCAT versions that don't fire the event
1614                Some(FirmwareEvent::BootAttempt)
1615            }
1616            Firmware::Uefi {
1617                guest: UefiGuest::None,
1618                ..
1619            }
1620            | Firmware::OpenhclUefi {
1621                guest: UefiGuest::None,
1622                ..
1623            } => Some(FirmwareEvent::NoBootDevice),
1624            Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => {
1625                Some(FirmwareEvent::BootSuccess)
1626            }
1627        }
1628    }
1629
1630    fn openhcl_config(&self) -> Option<&OpenHclConfig> {
1631        match self {
1632            Firmware::OpenhclLinuxDirect { openhcl_config, .. }
1633            | Firmware::OpenhclUefi { openhcl_config, .. }
1634            | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
1635            Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
1636        }
1637    }
1638
1639    fn openhcl_config_mut(&mut self) -> Option<&mut OpenHclConfig> {
1640        match self {
1641            Firmware::OpenhclLinuxDirect { openhcl_config, .. }
1642            | Firmware::OpenhclUefi { openhcl_config, .. }
1643            | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
1644            Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
1645        }
1646    }
1647
1648    fn uefi_config(&self) -> Option<&UefiConfig> {
1649        match self {
1650            Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
1651                Some(uefi_config)
1652            }
1653            Firmware::LinuxDirect { .. }
1654            | Firmware::OpenhclLinuxDirect { .. }
1655            | Firmware::Pcat { .. }
1656            | Firmware::OpenhclPcat { .. } => None,
1657        }
1658    }
1659
1660    fn uefi_config_mut(&mut self) -> Option<&mut UefiConfig> {
1661        match self {
1662            Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
1663                Some(uefi_config)
1664            }
1665            Firmware::LinuxDirect { .. }
1666            | Firmware::OpenhclLinuxDirect { .. }
1667            | Firmware::Pcat { .. }
1668            | Firmware::OpenhclPcat { .. } => None,
1669        }
1670    }
1671}
1672
1673/// The guest the VM will boot into. A boot drive with the chosen setup
1674/// will be automatically configured.
1675#[derive(Debug)]
1676pub enum PcatGuest {
1677    /// Mount a VHD as the boot drive.
1678    Vhd(BootImageConfig<boot_image_type::Vhd>),
1679    /// Mount an ISO as the CD/DVD drive.
1680    Iso(BootImageConfig<boot_image_type::Iso>),
1681}
1682
1683impl PcatGuest {
1684    fn artifact(&self) -> &ResolvedArtifact {
1685        match self {
1686            PcatGuest::Vhd(disk) => &disk.artifact,
1687            PcatGuest::Iso(disk) => &disk.artifact,
1688        }
1689    }
1690}
1691
1692/// The guest the VM will boot into. A boot drive with the chosen setup
1693/// will be automatically configured.
1694#[derive(Debug)]
1695pub enum UefiGuest {
1696    /// Mount a VHD as the boot drive.
1697    Vhd(BootImageConfig<boot_image_type::Vhd>),
1698    /// The UEFI test image produced by our guest-test infrastructure.
1699    GuestTestUefi(ResolvedArtifact),
1700    /// No guest, just the firmware.
1701    None,
1702}
1703
1704impl UefiGuest {
1705    /// Construct a standard [`UefiGuest::GuestTestUefi`] configuration.
1706    pub fn guest_test_uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
1707        use petri_artifacts_vmm_test::artifacts::test_vhd::*;
1708        let artifact = match arch {
1709            MachineArch::X86_64 => resolver.require(GUEST_TEST_UEFI_X64).erase(),
1710            MachineArch::Aarch64 => resolver.require(GUEST_TEST_UEFI_AARCH64).erase(),
1711        };
1712        UefiGuest::GuestTestUefi(artifact)
1713    }
1714
1715    fn artifact(&self) -> Option<&ResolvedArtifact> {
1716        match self {
1717            UefiGuest::Vhd(vhd) => Some(&vhd.artifact),
1718            UefiGuest::GuestTestUefi(p) => Some(p),
1719            UefiGuest::None => None,
1720        }
1721    }
1722}
1723
1724/// Type-tags for [`BootImageConfig`](super::BootImageConfig)
1725pub mod boot_image_type {
1726    mod private {
1727        pub trait Sealed {}
1728        impl Sealed for super::Vhd {}
1729        impl Sealed for super::Iso {}
1730    }
1731
1732    /// Private trait use to seal the set of artifact types BootImageType
1733    /// supports.
1734    pub trait BootImageType: private::Sealed {}
1735
1736    /// BootImageConfig for a VHD file
1737    #[derive(Debug)]
1738    pub enum Vhd {}
1739
1740    /// BootImageConfig for an ISO file
1741    #[derive(Debug)]
1742    pub enum Iso {}
1743
1744    impl BootImageType for Vhd {}
1745    impl BootImageType for Iso {}
1746}
1747
1748/// Configuration information for the boot drive of the VM.
1749#[derive(Debug)]
1750pub struct BootImageConfig<T: boot_image_type::BootImageType> {
1751    /// Artifact handle corresponding to the boot media.
1752    artifact: ResolvedArtifact,
1753    /// The OS flavor.
1754    os_flavor: OsFlavor,
1755    /// Any quirks needed to boot the guest.
1756    ///
1757    /// Most guests should not need any quirks, and can use `Default`.
1758    quirks: GuestQuirks,
1759    /// Marker denoting what type of media `artifact` corresponds to
1760    _type: core::marker::PhantomData<T>,
1761}
1762
1763impl BootImageConfig<boot_image_type::Vhd> {
1764    /// Create a new BootImageConfig from a VHD artifact handle
1765    pub fn from_vhd<A>(artifact: ResolvedArtifact<A>) -> Self
1766    where
1767        A: petri_artifacts_common::tags::IsTestVhd,
1768    {
1769        BootImageConfig {
1770            artifact: artifact.erase(),
1771            os_flavor: A::OS_FLAVOR,
1772            quirks: A::quirks(),
1773            _type: std::marker::PhantomData,
1774        }
1775    }
1776}
1777
1778impl BootImageConfig<boot_image_type::Iso> {
1779    /// Create a new BootImageConfig from an ISO artifact handle
1780    pub fn from_iso<A>(artifact: ResolvedArtifact<A>) -> Self
1781    where
1782        A: petri_artifacts_common::tags::IsTestIso,
1783    {
1784        BootImageConfig {
1785            artifact: artifact.erase(),
1786            os_flavor: A::OS_FLAVOR,
1787            quirks: A::quirks(),
1788            _type: std::marker::PhantomData,
1789        }
1790    }
1791}
1792
1793/// Isolation type
1794#[derive(Debug, Clone, Copy)]
1795pub enum IsolationType {
1796    /// VBS
1797    Vbs,
1798    /// SNP
1799    Snp,
1800    /// TDX
1801    Tdx,
1802}
1803
1804/// Flags controlling servicing behavior.
1805#[derive(Debug, Clone, Copy)]
1806pub struct OpenHclServicingFlags {
1807    /// Preserve DMA memory for NVMe devices if supported.
1808    /// Defaults to `true`.
1809    pub enable_nvme_keepalive: bool,
1810    /// 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.
1811    pub override_version_checks: bool,
1812    /// Hint to the OpenHCL runtime how much time to wait when stopping / saving the OpenHCL.
1813    pub stop_timeout_hint_secs: Option<u16>,
1814}
1815
1816/// Petri disk type
1817#[derive(Debug, Clone)]
1818pub enum PetriDiskType {
1819    /// Memory backed
1820    Memory,
1821    /// Memory differencing disk backed by a file
1822    Differencing(PathBuf),
1823    /// Persistent disk
1824    Persistent(PathBuf),
1825}
1826
1827/// Petri VMGS disk
1828#[derive(Debug, Clone)]
1829pub struct PetriVmgsDisk {
1830    /// Backing disk
1831    pub disk: PetriDiskType,
1832    /// Guest state encryption policy
1833    pub encryption_policy: GuestStateEncryptionPolicy,
1834}
1835
1836impl Default for PetriVmgsDisk {
1837    fn default() -> Self {
1838        PetriVmgsDisk {
1839            disk: PetriDiskType::Memory,
1840            // TODO: make this strict once we can set it in OpenHCL on Hyper-V
1841            encryption_policy: GuestStateEncryptionPolicy::None(false),
1842        }
1843    }
1844}
1845
1846/// Petri VM guest state resource
1847#[derive(Debug, Clone)]
1848pub enum PetriVmgsResource {
1849    /// Use disk to store guest state
1850    Disk(PetriVmgsDisk),
1851    /// Use disk to store guest state, reformatting if corrupted.
1852    ReprovisionOnFailure(PetriVmgsDisk),
1853    /// Format and use disk to store guest state
1854    Reprovision(PetriVmgsDisk),
1855    /// Store guest state in memory
1856    Ephemeral,
1857}
1858
1859impl PetriVmgsResource {
1860    /// get the inner vmgs disk if one exists
1861    pub fn disk(&self) -> Option<&PetriVmgsDisk> {
1862        match self {
1863            PetriVmgsResource::Disk(vmgs)
1864            | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1865            | PetriVmgsResource::Reprovision(vmgs) => Some(vmgs),
1866            PetriVmgsResource::Ephemeral => None,
1867        }
1868    }
1869}
1870
1871/// Petri VM guest state lifetime
1872#[derive(Debug, Clone, Copy)]
1873pub enum PetriGuestStateLifetime {
1874    /// Use a differencing disk backed by a blank, tempory VMGS file
1875    /// or other artifact if one is provided
1876    Disk,
1877    /// Same as default, except reformat the backing disk if corrupted
1878    ReprovisionOnFailure,
1879    /// Same as default, except reformat the backing disk
1880    Reprovision,
1881    /// Store guest state in memory (no backing disk)
1882    Ephemeral,
1883}
1884
1885/// UEFI secure boot template
1886#[derive(Debug, Clone, Copy)]
1887pub enum SecureBootTemplate {
1888    /// The Microsoft Windows template.
1889    MicrosoftWindows,
1890    /// The Microsoft UEFI certificate authority template.
1891    MicrosoftUefiCertificateAuthority,
1892}
1893
1894/// Quirks to workaround certain bugs that only manifest when using a
1895/// particular VMM, and do not depend on which guest is running.
1896#[derive(Default, Debug, Clone)]
1897pub struct VmmQuirks {
1898    /// Automatically reset the VM if we did not recieve a boot event in the
1899    /// specified amount of time.
1900    pub flaky_boot: Option<Duration>,
1901}
1902
1903/// Creates a VM-safe name that respects platform limitations.
1904///
1905/// Hyper-V limits VM names to 100 characters. For names that exceed this limit,
1906/// this function truncates to 96 characters and appends a 4-character hash
1907/// to ensure uniqueness while staying within the limit.
1908fn make_vm_safe_name(name: &str) -> String {
1909    const MAX_VM_NAME_LENGTH: usize = 100;
1910    const HASH_LENGTH: usize = 4;
1911    const MAX_PREFIX_LENGTH: usize = MAX_VM_NAME_LENGTH - HASH_LENGTH;
1912
1913    if name.len() <= MAX_VM_NAME_LENGTH {
1914        name.to_owned()
1915    } else {
1916        // Create a hash of the full name for uniqueness
1917        let mut hasher = DefaultHasher::new();
1918        name.hash(&mut hasher);
1919        let hash = hasher.finish();
1920
1921        // Format hash as a 4-character hex string
1922        let hash_suffix = format!("{:04x}", hash & 0xFFFF);
1923
1924        // Truncate the name and append the hash
1925        let truncated = &name[..MAX_PREFIX_LENGTH];
1926        tracing::debug!(
1927            "VM name too long ({}), truncating '{}' to '{}{}'",
1928            name.len(),
1929            name,
1930            truncated,
1931            hash_suffix
1932        );
1933
1934        format!("{}{}", truncated, hash_suffix)
1935    }
1936}
1937
1938/// The reason that the VM halted
1939#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1940pub enum PetriHaltReason {
1941    /// The vm powered off
1942    PowerOff,
1943    /// The vm reset
1944    Reset,
1945    /// The vm hibernated
1946    Hibernate,
1947    /// The vm triple faulted
1948    TripleFault,
1949    /// The vm halted for some other reason
1950    Other,
1951}
1952
1953fn append_cmdline(cmd: &mut Option<String>, add_cmd: impl AsRef<str>) {
1954    if let Some(cmd) = cmd.as_mut() {
1955        cmd.push(' ');
1956        cmd.push_str(add_cmd.as_ref());
1957    } else {
1958        *cmd = Some(add_cmd.as_ref().to_string());
1959    }
1960}
1961
1962async fn save_inspect(
1963    name: &str,
1964    inspect: std::pin::Pin<Box<dyn Future<Output = anyhow::Result<inspect::Node>> + Send>>,
1965    log_source: &PetriLogSource,
1966) {
1967    tracing::info!("Collecting {name} inspect details.");
1968    let node = match inspect.await {
1969        Ok(n) => n,
1970        Err(e) => {
1971            tracing::error!(?e, "Failed to get {name}");
1972            return;
1973        }
1974    };
1975    if let Err(e) = log_source.write_attachment(
1976        &format!("timeout_inspect_{name}.log"),
1977        format!("{node:#}").as_bytes(),
1978    ) {
1979        tracing::error!(?e, "Failed to save {name} inspect log");
1980        return;
1981    }
1982    tracing::info!("{name} inspect task finished.");
1983}
1984
1985#[cfg(test)]
1986mod tests {
1987    use super::make_vm_safe_name;
1988
1989    #[test]
1990    fn test_short_names_unchanged() {
1991        let short_name = "short_test_name";
1992        assert_eq!(make_vm_safe_name(short_name), short_name);
1993    }
1994
1995    #[test]
1996    fn test_exactly_100_chars_unchanged() {
1997        let name_100 = "a".repeat(100);
1998        assert_eq!(make_vm_safe_name(&name_100), name_100);
1999    }
2000
2001    #[test]
2002    fn test_long_name_truncated() {
2003        let long_name = "multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_openhcl_servicing";
2004        let result = make_vm_safe_name(long_name);
2005
2006        // Should be exactly 100 characters
2007        assert_eq!(result.len(), 100);
2008
2009        // Should start with the truncated prefix
2010        assert!(result.starts_with("multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_ope"));
2011
2012        // Should end with a 4-character hash
2013        let suffix = &result[96..];
2014        assert_eq!(suffix.len(), 4);
2015        // Should be valid hex
2016        assert!(u16::from_str_radix(suffix, 16).is_ok());
2017    }
2018
2019    #[test]
2020    fn test_deterministic_results() {
2021        let long_name = "very_long_test_name_that_exceeds_the_100_character_limit_and_should_be_truncated_consistently_every_time";
2022        let result1 = make_vm_safe_name(long_name);
2023        let result2 = make_vm_safe_name(long_name);
2024
2025        assert_eq!(result1, result2);
2026        assert_eq!(result1.len(), 100);
2027    }
2028
2029    #[test]
2030    fn test_different_names_different_hashes() {
2031        let name1 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_1";
2032        let name2 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_2";
2033
2034        let result1 = make_vm_safe_name(name1);
2035        let result2 = make_vm_safe_name(name2);
2036
2037        // Both should be 100 chars
2038        assert_eq!(result1.len(), 100);
2039        assert_eq!(result2.len(), 100);
2040
2041        // Should have different suffixes since the full names are different
2042        assert_ne!(result1, result2);
2043        assert_ne!(&result1[96..], &result2[96..]);
2044    }
2045}