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