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