1#[cfg(windows)]
6pub mod hyperv;
7pub mod openvmm;
9pub mod qemu;
11pub mod vtl2_settings;
12
13use crate::PetriLogSource;
14use crate::PetriTestParams;
15use crate::ShutdownKind;
16use crate::disk_image::AgentImage;
17use crate::disk_image::ImageEntry;
18use crate::disk_image::SECTOR_SIZE;
19use crate::mshv_boot::MshvBoot;
20use crate::openhcl_diag::OpenHclDiagHandler;
21use crate::test::PetriPostTestHook;
22use crate::vtl2_settings::ControllerType;
23use crate::vtl2_settings::Vtl2LunBuilder;
24use crate::vtl2_settings::Vtl2StorageBackingDeviceBuilder;
25use crate::vtl2_settings::Vtl2StorageControllerBuilder;
26use anyhow::Context;
27use async_trait::async_trait;
28use futures::FutureExt as _;
29use get_resources::ged::FirmwareEvent;
30use guid::Guid;
31use mesh::CancelContext;
32use openvmm_defs::config::Vtl2BaseAddressType;
33use pal_async::DefaultDriver;
34use pal_async::task::Spawn;
35use pal_async::task::Task;
36use pal_async::timer::PolledTimer;
37use petri_artifact_resolver_openvmm_known_paths::resolve_artifact;
38use petri_artifacts_common::tags::GuestQuirks;
39use petri_artifacts_common::tags::GuestQuirksInner;
40use petri_artifacts_common::tags::InitialRebootCondition;
41use petri_artifacts_common::tags::IsLoadable;
42use petri_artifacts_common::tags::IsOpenhclIgvm;
43use petri_artifacts_common::tags::IsTestVmgs;
44use petri_artifacts_common::tags::MachineArch;
45use petri_artifacts_common::tags::OsFlavor;
46use petri_artifacts_core::ArtifactHandle;
47use petri_artifacts_core::ArtifactId;
48use petri_artifacts_core::ArtifactResolver;
49use petri_artifacts_core::ArtifactSource;
50use petri_artifacts_core::ErasedArtifactHandle;
51use petri_artifacts_core::ResolvedArtifact;
52use petri_artifacts_core::ResolvedArtifactSource;
53use petri_artifacts_core::ResolvedOptionalArtifact;
54use petri_artifacts_core::env::PETRI_REMOTE_ARTIFACTS;
55use petri_artifacts_core::tags::IsNextestArchive;
56use petri_artifacts_vmm_test::env::*;
57use pipette_client::PipetteClient;
58use std::collections::BTreeMap;
59use std::collections::HashMap;
60use std::fmt::Debug;
61use std::io::Write;
62use std::panic::AssertUnwindSafe;
63use std::path::Path;
64use std::path::PathBuf;
65use std::path::absolute;
66use std::sync::Arc;
67use std::time::Duration;
68use tempfile::TempPath;
69use vmgs_resources::GuestStateEncryptionPolicy;
70use vtl2_settings_proto::StorageController;
71use vtl2_settings_proto::Vtl2Settings;
72
73pub struct PetriVmArtifacts<T: PetriVmmBackend> {
76 pub backend: T,
78 pub firmware: Firmware,
80 pub arch: MachineArch,
82 pub agent_image: Option<AgentImage>,
84 pub openhcl_agent_image: Option<AgentImage>,
86 pub pipette_binary: Option<ResolvedArtifact>,
88 pub nested_test: Option<NestedTestDeps>,
90}
91
92impl<T: PetriVmmBackend> PetriVmArtifacts<T> {
93 pub fn new(
97 resolver: &ArtifactResolver<'_>,
98 firmware: Firmware,
99 arch: MachineArch,
100 with_vtl0_pipette: bool,
101 ) -> Option<Self> {
102 Self::new_maybe_nested(resolver, firmware, arch, with_vtl0_pipette, None)
103 }
104
105 pub fn new_maybe_nested(
109 resolver: &ArtifactResolver<'_>,
110 firmware: Firmware,
111 arch: MachineArch,
112 with_vtl0_pipette: bool,
113 nested_test: Option<NestedTestDeps>,
114 ) -> Option<Self> {
115 let different_nested_target = nested_test
119 .as_ref()
120 .map(|t| t.archive.target_triple().unwrap())
121 .is_some_and(|t| {
122 let host_target = target_lexicon::Triple::host();
123 host_target.operating_system != t.operating_system
124 });
125
126 if !(T::SUPPORTS_CPU_EMULATION || arch == MachineArch::host())
127 || !T::check_compat(&firmware, arch)
128 || different_nested_target && (cfg!(windows) || !T::supports_file_sharing(&firmware))
129 {
130 return None;
131 }
132
133 let pipette_binary = if with_vtl0_pipette {
134 Some(Self::resolve_pipette_binary(
135 resolver,
136 firmware.os_flavor(),
137 arch,
138 ))
139 } else {
140 None
141 };
142
143 Some(Self {
144 backend: T::new(resolver, arch),
145 arch,
146 agent_image: (!firmware.is_isolated_without_paravisor()).then(|| {
147 if with_vtl0_pipette {
148 AgentImage::new(firmware.os_flavor()).with_pipette(resolver, arch)
149 } else {
150 AgentImage::new(firmware.os_flavor())
151 }
152 }),
153 openhcl_agent_image: if firmware.is_openhcl() {
154 Some(AgentImage::new(OsFlavor::Linux).with_pipette(resolver, arch))
155 } else {
156 None
157 },
158 pipette_binary,
159 firmware,
160 nested_test,
161 })
162 }
163
164 fn resolve_pipette_binary(
165 resolver: &ArtifactResolver<'_>,
166 os_flavor: OsFlavor,
167 arch: MachineArch,
168 ) -> ResolvedArtifact {
169 use petri_artifacts_common::artifacts as common_artifacts;
170 match (os_flavor, arch) {
171 (OsFlavor::Linux, MachineArch::X86_64) => resolver
172 .require(common_artifacts::PIPETTE_LINUX_X64_MUSL)
173 .erase(),
174 (OsFlavor::Linux, MachineArch::Aarch64) => resolver
175 .require(common_artifacts::PIPETTE_LINUX_AARCH64_MUSL)
176 .erase(),
177 (OsFlavor::Windows, MachineArch::X86_64) => resolver
178 .require(common_artifacts::PIPETTE_WINDOWS_X64)
179 .erase(),
180 (OsFlavor::Windows, MachineArch::Aarch64) => resolver
181 .require(common_artifacts::PIPETTE_WINDOWS_AARCH64)
182 .erase(),
183 (OsFlavor::FreeBsd | OsFlavor::Uefi, _) => {
184 panic!("No pipette binary for this OS flavor")
185 }
186 }
187 }
188}
189
190pub struct PetriVmBuilder<T: PetriVmmBackend> {
192 backend: T,
194 config: PetriVmConfig,
196 modify_vmm_config: Option<ModifyFn<T::VmmConfig>>,
198 resources: PetriVmResources,
200
201 guest_quirks: GuestQuirksInner,
203 vmm_quirks: VmmQuirks,
204
205 expected_boot_event: Option<FirmwareEvent>,
208 override_expect_reset: bool,
209
210 agent_image: Option<AgentImage>,
214 openhcl_agent_image: Option<AgentImage>,
216 boot_device_type: BootDeviceType,
218 pcie_boot_port: Option<String>,
221
222 minimal_mode: bool,
224 pipette_binary: Option<ResolvedArtifact>,
226 enable_serial: bool,
228 use_virtio_console: bool,
229 enable_screenshots: bool,
231 use_virtio_vsock: bool,
233 #[cfg(target_os = "linux")]
235 vhost_vsock_guest_cid: Option<u32>,
236 no_vmbus: bool,
238 no_hv: bool,
240 capture_inspect_on_failure: bool,
242 watchdog_timeout: Option<Duration>,
243 external_pipette: bool,
244
245 nested_test: Option<ResolvedNestedTestDeps>,
246}
247
248const INSPECT_TIMEOUT: Duration = Duration::from_secs(10);
250
251impl<T: PetriVmmBackend> Debug for PetriVmBuilder<T> {
252 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253 f.debug_struct("PetriVmBuilder")
254 .field("backend", &self.backend)
255 .field("config", &self.config)
256 .field("modify_vmm_config", &self.modify_vmm_config.is_some())
257 .field("resources", &self.resources)
258 .field("guest_quirks", &self.guest_quirks)
259 .field("vmm_quirks", &self.vmm_quirks)
260 .field("expected_boot_event", &self.expected_boot_event)
261 .field("override_expect_reset", &self.override_expect_reset)
262 .field("agent_image", &self.agent_image)
263 .field("openhcl_agent_image", &self.openhcl_agent_image)
264 .field("boot_device_type", &self.boot_device_type)
265 .field("pcie_boot_port", &self.pcie_boot_port)
266 .field("minimal_mode", &self.minimal_mode)
267 .field("enable_serial", &self.enable_serial)
268 .field("use_virtio_console", &self.use_virtio_console)
269 .field("enable_screenshots", &self.enable_screenshots)
270 .field("use_virtio_vsock", &self.use_virtio_vsock)
271 .field("no_vmbus", &self.no_vmbus)
272 .field("no_hv", &self.no_hv)
273 .finish()
274 }
275}
276
277#[derive(Debug, Clone)]
279pub struct PetriVmConfig {
280 pub name: String,
282 pub arch: MachineArch,
284 pub host_log_levels: Option<OpenvmmLogConfig>,
286 pub firmware: Firmware,
288 pub hibernation_enabled: bool,
290 pub ipmi_enabled: bool,
292 pub memory: MemoryConfig,
294 pub proc_topology: ProcessorTopology,
296 pub nested_virt_enabled: bool,
298 pub vmgs: PetriVmgsResource,
300 pub tpm: Option<TpmConfig>,
302 pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
304 pub pcie_nvme_drives: Vec<PcieNvmeDrive>,
306 pub pcie_virtio_blk_drives: Vec<PcieVirtioBlkDrive>,
308 pub physical_nvme_devices: HashMap<Guid, PhysicalNvmeDevice>,
310 pub guest_shares: HashMap<String, GuestShare>,
312}
313
314#[derive(Debug, Clone)]
316pub struct GuestShare {
317 pub host_path: PathBuf,
319 pub guest_path: String,
321}
322
323#[derive(Debug, Clone)]
325pub struct PcieNvmeDrive {
326 pub port_name: String,
328 pub nsid: u32,
330 pub drive: Drive,
332}
333
334#[derive(Debug, Clone)]
336pub struct PcieVirtioBlkDrive {
337 pub port_name: String,
339 pub drive: Drive,
341}
342
343#[derive(Debug, Clone)]
346pub struct PhysicalNvmeDevice {
347 pub target_vtl: Vtl,
349 pub nsid: u32,
351 pub namespace_size_mib: u64,
353}
354
355#[derive(Clone)]
358pub struct PetriVmProperties {
359 pub is_openhcl: bool,
361 pub is_isolated: bool,
363 pub is_pcat: bool,
365 pub is_linux_direct: bool,
367 pub using_vtl0_pipette: bool,
369 pub external_pipette: bool,
371 pub using_vpci: bool,
373 pub os_flavor: OsFlavor,
375 pub minimal_mode: bool,
377 pub uses_pipette_as_init: bool,
379 pub enable_serial: bool,
381 pub use_virtio_console: bool,
383 pub has_agent_disk: bool,
385 pub use_virtio_vsock: bool,
387 #[cfg(target_os = "linux")]
389 pub vhost_vsock_guest_cid: Option<u32>,
390 pub no_vmbus: bool,
392 pub no_hv: bool,
394}
395
396pub struct PetriVmRuntimeConfig {
398 pub vtl2_settings: Option<Vtl2Settings>,
400 pub ide_controllers: Option<[[Option<Drive>; 2]; 2]>,
402 pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
404}
405
406#[derive(Debug)]
408pub struct PetriVmResources {
409 pub driver: DefaultDriver,
411 pub log_source: PetriLogSource,
413 pub prebuilt_initrd: Option<PetriInitrd>,
415}
416
417#[async_trait]
419pub trait PetriVmmBackend: Debug {
420 type VmmConfig: 'static;
422
423 type VmRuntime: PetriVmRuntime;
425
426 const SUPPORTS_VMBUS: bool;
428
429 const SUPPORTS_CPU_EMULATION: bool = false;
434
435 const SUPPORTS_FILE_SHARING: bool = false;
437
438 fn supports_file_sharing(firmware: &Firmware) -> bool {
440 Self::SUPPORTS_FILE_SHARING && firmware.os_flavor() == OsFlavor::Linux
441 }
442
443 fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool;
452
453 fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks);
455
456 fn default_servicing_flags() -> OpenHclServicingFlags;
458
459 fn create_guest_dump_disk() -> anyhow::Result<
462 Option<(
463 Arc<TempPath>,
464 Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
465 )>,
466 >;
467
468 fn build_custom_init_script(pipette_path: &str) -> Option<String>;
471
472 fn new(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self;
477
478 async fn run(
480 self,
481 config: PetriVmConfig,
482 modify_vmm_config: Option<ModifyFn<Self::VmmConfig>>,
483 resources: &PetriVmResources,
484 properties: &PetriVmProperties,
485 ) -> anyhow::Result<(Self::VmRuntime, PetriVmRuntimeConfig)>;
486}
487
488pub(crate) const PETRI_IDE_BOOT_CONTROLLER_NUMBER: u32 = 0;
490pub(crate) const PETRI_IDE_BOOT_LUN: u8 = 0;
491pub(crate) const PETRI_IDE_BOOT_CONTROLLER: Guid =
492 guid::guid!("ca56751f-e643-4bef-bf54-f73678e8b7b5");
493
494pub(crate) const PETRI_SCSI_BOOT_LUN: u32 = 0;
496pub(crate) const PETRI_SCSI_PIPETTE_LUN: u32 = 1;
497pub(crate) const PETRI_SCSI_CRASH_LUN: u32 = 2;
498pub(crate) const PETRI_SCSI_VTL0_CONTROLLER: Guid =
500 guid::guid!("27b553e8-8b39-411b-a55f-839971a7884f");
501pub(crate) const PETRI_SCSI_VTL2_CONTROLLER: Guid =
503 guid::guid!("766e96f8-2ceb-437e-afe3-a93169e48a7c");
504pub(crate) const PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER: Guid =
506 guid::guid!("6c474f47-ed39-49e6-bbb9-142177a1da6e");
507
508pub(crate) const PETRI_NVME_BOOT_NSID: u32 = 37;
510pub(crate) const PETRI_NVME_BOOT_VTL0_CONTROLLER: Guid =
512 guid::guid!("e23a04e2-90f5-4852-bc9d-e7ac691b756c");
513pub(crate) const PETRI_NVME_BOOT_VTL2_CONTROLLER: Guid =
515 guid::guid!("92bc8346-718b-449a-8751-edbf3dcd27e4");
516
517pub(crate) const PETRI_PCIE_NVME_AGENT_PORT: &str = "s0rc0rp1";
519pub(crate) const PETRI_PCIE_NVME_AGENT_NSID: u32 = 1;
521
522const NESTED_TEST_CONTENT_DIR: &str = "vmm_tests";
523const NESTED_IMAGES_DIR: &str = "images";
524const NESTED_RESULTS_DIR: &str = "test_results";
525const NESTED_TEST_BINARY: &str = "nested_test";
526const NESTED_TEMP_DIR: &str = "temp";
527const NESTED_CONTENT_TAG: &str = "testcontent";
528const NESTED_IMAGES_TAG: &str = "testimages";
529
530pub struct PetriVm<T: PetriVmmBackend> {
532 resources: PetriVmResources,
533 runtime: PetriVmRuntimeGuard<T::VmRuntime>,
534 watchdog_tasks: Vec<Task<()>>,
535 openhcl_diag_handler: Option<OpenHclDiagHandler>,
536
537 arch: MachineArch,
538 guest_quirks: GuestQuirksInner,
539 vmm_quirks: VmmQuirks,
540 expected_boot_event: Option<FirmwareEvent>,
541
542 config: PetriVmRuntimeConfig,
543 properties: PetriVmProperties,
544
545 nested_test: Option<ResolvedNestedTestDeps>,
546 guest_shares: HashMap<String, GuestShare>,
547 name: String,
548}
549
550struct PetriVmRuntimeGuard<T: PetriVmRuntime> {
554 runtime: Option<T>,
555 driver: DefaultDriver,
556 log_source: PetriLogSource,
557 capture_inspect_on_drop: bool,
558}
559
560impl<T: PetriVmRuntime> PetriVmRuntimeGuard<T> {
561 fn take_for_teardown(&mut self) -> T {
562 self.runtime.take().expect("runtime has already been taken")
563 }
564}
565
566impl<T: PetriVmRuntime> std::ops::Deref for PetriVmRuntimeGuard<T> {
567 type Target = T;
568
569 fn deref(&self) -> &T {
570 self.runtime
571 .as_ref()
572 .expect("runtime has already been taken")
573 }
574}
575
576impl<T: PetriVmRuntime> std::ops::DerefMut for PetriVmRuntimeGuard<T> {
577 fn deref_mut(&mut self) -> &mut T {
578 self.runtime
579 .as_mut()
580 .expect("runtime has already been taken")
581 }
582}
583
584impl<T: PetriVmRuntime> Drop for PetriVmRuntimeGuard<T> {
585 fn drop(&mut self) {
586 let Some(runtime) = self.runtime.take() else {
587 return;
588 };
589 if !self.capture_inspect_on_drop {
590 return;
591 }
592 let inspector = runtime.inspector();
593 let openhcl_diag_handler = runtime.openhcl_diag();
594 if inspector.is_none() && openhcl_diag_handler.is_none() {
595 return;
596 }
597 let log_source = self.log_source.clone();
598
599 let capture = async move {
600 let vmm = async {
601 if let Some(inspector) = &inspector {
602 collect_inspect("vmm", inspector.inspect(""), &log_source, "failure").await;
603 }
604 };
605 let openhcl = async {
606 if let Some(diag) = &openhcl_diag_handler {
607 collect_inspect(
608 "openhcl",
609 diag.inspect("", None, None),
610 &log_source,
611 "failure",
612 )
613 .await;
614 }
615 };
616 futures::future::join(vmm, openhcl).await;
617 drop(runtime);
618 };
619
620 self.driver
624 .spawn("petri-inspect-on-drop", async move {
625 if let Err(e) = AssertUnwindSafe(capture).catch_unwind().await {
628 tracing::error!(?e, "panicked while collecting inspect state");
629 }
630 })
631 .detach();
632 }
633}
634
635impl<T: PetriVmmBackend> PetriVmBuilder<T> {
636 pub fn new(
638 params: PetriTestParams<'_>,
639 artifacts: PetriVmArtifacts<T>,
640 driver: &DefaultDriver,
641 ) -> anyhow::Result<Self> {
642 let isolated_without_paravisor = artifacts.firmware.is_isolated_without_paravisor();
643 let builder = Self::minimal(params.test_name, params.log_source, artifacts, driver)?
644 .clear_minimal_mode()
645 .with_serial_output()
646 .with_capture_inspect_on_failure();
647 if isolated_without_paravisor {
648 Ok(builder)
649 } else {
650 builder
651 .add_petri_scsi_controllers()
652 .add_guest_crash_disk(params.post_test_hooks)
653 }
654 }
655
656 pub fn minimal(
667 test_name: &str,
668 log_source: &PetriLogSource,
669 artifacts: PetriVmArtifacts<T>,
670 driver: &DefaultDriver,
671 ) -> anyhow::Result<Self> {
672 let isolated_without_paravisor = artifacts.firmware.is_isolated_without_paravisor();
673 match &artifacts.firmware {
674 Firmware::Uefi {
675 guest: UefiGuest::Mshv(_),
676 ..
677 } => anyhow::ensure!(
678 artifacts.arch == MachineArch::X86_64,
679 "mshv boot requires x86-64 UEFI"
680 ),
681 Firmware::OpenhclUefi {
682 guest: UefiGuest::Mshv(_),
683 ..
684 } => anyhow::bail!("mshv boot with OpenHCL is not supported"),
685 _ => {}
686 }
687 let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
688 let expected_boot_event = artifacts.firmware.expected_boot_event();
689 let boot_device_type = match artifacts.firmware {
690 Firmware::LinuxDirect { .. } | Firmware::IgvmLinuxDirect { .. } => BootDeviceType::None,
691 Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
692 Firmware::Pcat { .. } => BootDeviceType::Ide,
693 Firmware::OpenhclPcat { .. } => BootDeviceType::IdeViaScsi,
694 Firmware::Uefi {
695 guest: UefiGuest::None,
696 ..
697 }
698 | Firmware::OpenhclUefi {
699 guest: UefiGuest::None,
700 ..
701 } => BootDeviceType::None,
702 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
703 };
704
705 let supports_file_sharing = T::supports_file_sharing(&artifacts.firmware);
706 let external_pipette = isolated_without_paravisor && artifacts.pipette_binary.is_some();
707 let builder = Self {
708 backend: artifacts.backend,
709 config: PetriVmConfig {
710 name: test_name.to_string(),
711 arch: artifacts.arch,
712 host_log_levels: None,
713 firmware: artifacts.firmware,
714 hibernation_enabled: false,
715 ipmi_enabled: false,
716 memory: Default::default(),
717 proc_topology: Default::default(),
718 nested_virt_enabled: false,
719
720 vmgs: PetriVmgsResource::Ephemeral,
721 tpm: None,
722 vmbus_storage_controllers: HashMap::new(),
723 pcie_nvme_drives: Vec::new(),
724 pcie_virtio_blk_drives: Vec::new(),
725 physical_nvme_devices: HashMap::new(),
726 guest_shares: HashMap::new(),
727 },
728 modify_vmm_config: None,
729 resources: PetriVmResources {
730 driver: driver.clone(),
731 log_source: log_source.clone(),
732 prebuilt_initrd: None,
733 },
734
735 guest_quirks,
736 vmm_quirks,
737 expected_boot_event,
738 override_expect_reset: false,
739
740 agent_image: artifacts.agent_image,
741 openhcl_agent_image: artifacts.openhcl_agent_image,
742 boot_device_type,
743 pcie_boot_port: None,
744
745 minimal_mode: true,
746 pipette_binary: artifacts.pipette_binary,
747 enable_serial: false,
748 use_virtio_console: false,
749 enable_screenshots: true,
750 use_virtio_vsock: isolated_without_paravisor,
751 #[cfg(target_os = "linux")]
752 vhost_vsock_guest_cid: None,
753 no_vmbus: !T::SUPPORTS_VMBUS || isolated_without_paravisor,
754 no_hv: false,
755 capture_inspect_on_failure: false,
756 watchdog_timeout: Some(Duration::from_mins(10)),
757 external_pipette: false,
758
759 nested_test: artifacts
760 .nested_test
761 .map(|t| t.resolve(!supports_file_sharing))
762 .transpose()?,
763 };
764 Ok(if external_pipette {
765 builder.with_external_pipette()
766 } else {
767 builder
768 })
769 }
770
771 fn clear_minimal_mode(mut self) -> Self {
772 self.minimal_mode = false;
773 self
774 }
775
776 pub fn with_shared_directory(
780 mut self,
781 tag: &str,
782 host_path: impl AsRef<Path>,
783 guest_path: &str,
784 ) -> Self {
785 assert!(
786 T::supports_file_sharing(&self.config.firmware),
787 "directory sharing requires a backend with Linux file-sharing support"
788 );
789 assert!(
790 !tag.is_empty()
791 && !tag.starts_with('-')
792 && tag
793 .bytes()
794 .all(|c| c.is_ascii_alphanumeric() || b"-_".contains(&c)),
795 "invalid filesystem share tag"
796 );
797 assert!(
798 guest_path.starts_with('/')
799 && !guest_path.contains('\0')
800 && !guest_path.split('/').any(|c| c == ".."),
801 "guest mount point must be an absolute path without parent traversal"
802 );
803 assert!(
804 !self.config.guest_shares.contains_key(tag),
805 "duplicate filesystem share tag: {tag}"
806 );
807 self.config.guest_shares.insert(
808 tag.into(),
809 GuestShare {
810 host_path: host_path.as_ref().to_path_buf(),
811 guest_path: guest_path.into(),
812 },
813 );
814 self
815 }
816
817 fn with_capture_inspect_on_failure(mut self) -> Self {
818 self.capture_inspect_on_failure = true;
819 self
820 }
821
822 pub fn is_minimal(&self) -> bool {
824 self.minimal_mode
825 }
826
827 pub fn with_watchdog_timeout(mut self, timeout: Option<Duration>) -> Self {
830 self.watchdog_timeout = timeout;
831 self
832 }
833
834 pub fn with_external_pipette(mut self) -> Self {
840 self.external_pipette = true;
841 self.agent_image = None;
842 self.pipette_binary = None;
843 self
844 }
845
846 pub fn with_prebuilt_initrd(mut self, initrd: PetriInitrd) -> Self {
853 self.resources.prebuilt_initrd = Some(initrd);
854 self
855 }
856
857 pub fn prepare_initrd(&self) -> anyhow::Result<PetriInitrd> {
868 self.prepare_custom_initrd(T::build_custom_init_script)
869 }
870
871 pub fn prepare_custom_initrd(
873 &self,
874 build_custom_init_script: impl FnOnce(&str) -> Option<String>,
875 ) -> anyhow::Result<PetriInitrd> {
876 const PIPETTE_PATH: &str = "pipette";
877 const INIT_SCRIPT_NAME: &str = "custom-init.sh";
878
879 let initrd_path = self
880 .config
881 .firmware
882 .linux_direct_initrd()
883 .context("prepare_initrd requires Linux direct boot with initrd")?;
884 let pipette_path = self
885 .pipette_binary
886 .as_ref()
887 .context("prepare_initrd requires a pipette binary")?;
888
889 let pipette_data = std::fs::read(pipette_path.get()).with_context(|| {
890 format!(
891 "failed to read pipette binary at {}",
892 pipette_path.get().display()
893 )
894 })?;
895
896 let mut files = vec![crate::initrd::InitrdFile {
897 name: PIPETTE_PATH,
898 contents: &pipette_data,
899 permissions: 0o755,
900 }];
901 let script = build_custom_init_script(PIPETTE_PATH);
902 let rdinit = if let Some(script) = &script {
903 files.push(crate::initrd::InitrdFile {
904 name: INIT_SCRIPT_NAME,
905 contents: script.as_bytes(),
906 permissions: 0o755,
907 });
908 format!("/{INIT_SCRIPT_NAME}")
909 } else {
910 format!("/{PIPETTE_PATH}")
911 };
912 PetriInitrd::prepare(initrd_path, &files, &rdinit, None)
913 }
914
915 pub fn with_serial_output(mut self) -> Self {
924 self.enable_serial = true;
925 self
926 }
927
928 pub fn without_serial_output(mut self) -> Self {
933 self.enable_serial = false;
934 self
935 }
936
937 pub fn without_screenshots(mut self) -> Self {
942 self.enable_screenshots = false;
943 self
944 }
945
946 pub fn with_virtio_vsock(mut self, enable: bool) -> Self {
959 self.use_virtio_vsock = enable;
960 #[cfg(target_os = "linux")]
961 {
962 self.vhost_vsock_guest_cid = None;
963 }
964 self
965 }
966
967 #[cfg(target_os = "linux")]
973 pub fn with_vhost_vsock(mut self, guest_cid: u32) -> Self {
974 assert!(
975 (3..u32::MAX).contains(&guest_cid),
976 "vhost-vsock guest CID must be between 3 and {}",
977 u32::MAX - 1
978 );
979 self.use_virtio_vsock = true;
980 self.vhost_vsock_guest_cid = Some(guest_cid);
981 self
982 }
983
984 pub fn with_no_vmbus(mut self) -> Self {
992 self.no_vmbus = true;
993 if self.config.firmware.os_flavor() != OsFlavor::Windows {
994 self.use_virtio_vsock = true;
995 }
996 self.config.vmbus_storage_controllers.clear();
997 self
998 }
999
1000 pub fn with_no_hv(mut self) -> Self {
1006 self.no_hv = true;
1007 self.with_no_vmbus()
1008 }
1009
1010 fn add_petri_scsi_controllers(mut self) -> Self {
1011 if T::SUPPORTS_VMBUS {
1012 self = self.add_vmbus_storage_controller(
1013 &PETRI_SCSI_VTL0_CONTROLLER,
1014 Vtl::Vtl0,
1015 VmbusStorageType::Scsi,
1016 );
1017
1018 if self.is_openhcl() {
1019 self = self.add_vmbus_storage_controller(
1020 &PETRI_SCSI_VTL2_CONTROLLER,
1021 Vtl::Vtl2,
1022 VmbusStorageType::Scsi,
1023 )
1024 }
1025 }
1026 self
1027 }
1028
1029 fn add_guest_crash_disk(
1030 self,
1031 post_test_hooks: &mut Vec<PetriPostTestHook>,
1032 ) -> anyhow::Result<Self> {
1033 let supports_vmbus_crash_disk = !self.no_vmbus
1034 && matches!(
1035 self.config.firmware.os_flavor(),
1036 OsFlavor::Windows | OsFlavor::Linux
1037 );
1038
1039 let (disk, disk_hook) = supports_vmbus_crash_disk
1040 .then(|| T::create_guest_dump_disk().context("failed to create guest dump disk"))
1041 .transpose()?
1042 .flatten()
1043 .unzip();
1044
1045 if let Some(disk_hook) = disk_hook {
1046 let logger = self.resources.log_source.clone();
1047 post_test_hooks.push(PetriPostTestHook::new(
1048 "extract guest crash dumps".into(),
1049 move |test_passed| {
1050 if test_passed {
1051 return Ok(());
1052 }
1053 let mut disk = disk_hook()?;
1054 let gpt = gptman::GPT::read_from(&mut disk, SECTOR_SIZE)?;
1055 let partition = fscommon::StreamSlice::new(
1056 &mut disk,
1057 gpt[1].starting_lba * SECTOR_SIZE,
1058 gpt[1].ending_lba * SECTOR_SIZE,
1059 )?;
1060 let fs = fatfs::FileSystem::new(partition, fatfs::FsOptions::new())?;
1061 for entry in fs.root_dir().iter() {
1062 let Ok(entry) = entry else {
1063 tracing::warn!(?entry, "failed to read entry in guest crash dump disk");
1064 continue;
1065 };
1066 if !entry.is_file() {
1067 tracing::warn!(
1068 ?entry,
1069 "skipping non-file entry in guest crash dump disk"
1070 );
1071 continue;
1072 }
1073 logger.write_attachment(&entry.file_name(), entry.to_file())?;
1074 }
1075 Ok(())
1076 },
1077 ));
1078 }
1079
1080 Ok(if let Some(disk) = disk {
1081 self.add_vmbus_drive(
1082 Drive::new(Some(Disk::Temporary(disk)), false),
1083 &PETRI_SCSI_VTL0_CONTROLLER,
1084 Some(PETRI_SCSI_CRASH_LUN),
1085 )
1086 } else {
1087 self
1088 })
1089 }
1090
1091 fn add_agent_disks(self) -> anyhow::Result<Self> {
1092 self.add_agent_disk_inner(Vtl::Vtl0)?
1093 .add_agent_disk_inner(Vtl::Vtl2)
1094 }
1095
1096 fn add_agent_disk_inner(mut self, target_vtl: Vtl) -> anyhow::Result<Self> {
1097 let (agent_image, controller_id) = match target_vtl {
1098 Vtl::Vtl0 => (self.agent_image.as_ref(), PETRI_SCSI_VTL0_CONTROLLER),
1099 Vtl::Vtl1 => panic!("no VTL1 agent disk"),
1100 Vtl::Vtl2 => (
1101 self.openhcl_agent_image.as_ref(),
1102 PETRI_SCSI_VTL2_CONTROLLER,
1103 ),
1104 };
1105
1106 if target_vtl == Vtl::Vtl0
1109 && self.uses_pipette_as_init()
1110 && !agent_image.is_some_and(|i| i.has_extras())
1111 {
1112 return Ok(self);
1113 }
1114
1115 let Some(agent_disk) = agent_image
1116 .map(|i| {
1117 i.build(crate::disk_image::ImageType::Vhd)
1118 .context("failed to build agent image")
1119 })
1120 .transpose()?
1121 .flatten()
1122 else {
1123 return Ok(self);
1124 };
1125
1126 if self.no_vmbus {
1129 self.config.pcie_nvme_drives.push(PcieNvmeDrive {
1130 port_name: PETRI_PCIE_NVME_AGENT_PORT.into(),
1131 nsid: PETRI_PCIE_NVME_AGENT_NSID,
1132 drive: Drive::new(
1133 Some(Disk::Temporary(Arc::new(agent_disk.into_temp_path()))),
1134 false,
1135 ),
1136 });
1137 return Ok(self);
1138 }
1139
1140 if !self
1143 .config
1144 .vmbus_storage_controllers
1145 .contains_key(&controller_id)
1146 {
1147 self = self.add_vmbus_storage_controller(
1148 &controller_id,
1149 target_vtl,
1150 VmbusStorageType::Scsi,
1151 );
1152 }
1153
1154 Ok(self.add_vmbus_drive(
1155 Drive::new(
1156 Some(Disk::Temporary(Arc::new(agent_disk.into_temp_path()))),
1157 false,
1158 ),
1159 &controller_id,
1160 Some(PETRI_SCSI_PIPETTE_LUN),
1161 ))
1162 }
1163
1164 fn add_boot_disk(mut self) -> anyhow::Result<Self> {
1165 if self.boot_device_type.requires_vtl2() && !self.is_openhcl() {
1166 panic!("boot device type {:?} requires vtl2", self.boot_device_type);
1167 }
1168
1169 if self.no_vmbus && self.boot_device_type.requires_vmbus() {
1170 panic!(
1171 "boot device type {:?} requires vmbus, but vmbus is disabled; \
1172 use with_boot_device_type(BootDeviceType::PcieNvme) or similar",
1173 self.boot_device_type
1174 );
1175 }
1176
1177 if self.boot_device_type.requires_vpci_boot() {
1178 self.config
1179 .firmware
1180 .uefi_config_mut()
1181 .expect("vpci boot requires uefi")
1182 .enable_vpci_boot = true;
1183 }
1184
1185 let builder = if let Some(boot_drive) =
1186 self.config.firmware.boot_drive(self.use_virtio_vsock)?
1187 {
1188 match self.boot_device_type {
1189 BootDeviceType::None => unreachable!(),
1190 BootDeviceType::Ide => self.add_ide_drive(
1191 boot_drive,
1192 PETRI_IDE_BOOT_CONTROLLER_NUMBER,
1193 PETRI_IDE_BOOT_LUN,
1194 ),
1195 BootDeviceType::IdeViaScsi => self
1196 .add_vmbus_drive(
1197 boot_drive,
1198 &PETRI_SCSI_VTL2_CONTROLLER,
1199 Some(PETRI_SCSI_BOOT_LUN),
1200 )
1201 .add_vtl2_storage_controller(
1202 Vtl2StorageControllerBuilder::new(ControllerType::Ide)
1203 .with_instance_id(PETRI_IDE_BOOT_CONTROLLER)
1204 .add_lun(
1205 Vtl2LunBuilder::disk()
1206 .with_channel(PETRI_IDE_BOOT_CONTROLLER_NUMBER)
1207 .with_location(PETRI_IDE_BOOT_LUN as u32)
1208 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
1209 ControllerType::Scsi,
1210 PETRI_SCSI_VTL2_CONTROLLER,
1211 PETRI_SCSI_BOOT_LUN,
1212 )),
1213 )
1214 .build(),
1215 ),
1216 BootDeviceType::IdeViaNvme => todo!(),
1217 BootDeviceType::Scsi => self.add_vmbus_drive(
1218 boot_drive,
1219 &PETRI_SCSI_VTL0_CONTROLLER,
1220 Some(PETRI_SCSI_BOOT_LUN),
1221 ),
1222 BootDeviceType::ScsiViaScsi => self
1223 .add_vmbus_drive(
1224 boot_drive,
1225 &PETRI_SCSI_VTL2_CONTROLLER,
1226 Some(PETRI_SCSI_BOOT_LUN),
1227 )
1228 .add_vtl2_storage_controller(
1229 Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
1230 .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
1231 .add_lun(
1232 Vtl2LunBuilder::disk()
1233 .with_location(PETRI_SCSI_BOOT_LUN)
1234 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
1235 ControllerType::Scsi,
1236 PETRI_SCSI_VTL2_CONTROLLER,
1237 PETRI_SCSI_BOOT_LUN,
1238 )),
1239 )
1240 .build(),
1241 ),
1242 BootDeviceType::ScsiViaNvme => self
1243 .add_vmbus_storage_controller(
1244 &PETRI_NVME_BOOT_VTL2_CONTROLLER,
1245 Vtl::Vtl2,
1246 VmbusStorageType::Nvme,
1247 )
1248 .add_vmbus_drive(
1249 boot_drive,
1250 &PETRI_NVME_BOOT_VTL2_CONTROLLER,
1251 Some(PETRI_NVME_BOOT_NSID),
1252 )
1253 .add_vtl2_storage_controller(
1254 Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
1255 .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
1256 .add_lun(
1257 Vtl2LunBuilder::disk()
1258 .with_location(PETRI_SCSI_BOOT_LUN)
1259 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
1260 ControllerType::Nvme,
1261 PETRI_NVME_BOOT_VTL2_CONTROLLER,
1262 PETRI_NVME_BOOT_NSID,
1263 )),
1264 )
1265 .build(),
1266 ),
1267 BootDeviceType::Nvme => self
1268 .add_vmbus_storage_controller(
1269 &PETRI_NVME_BOOT_VTL0_CONTROLLER,
1270 Vtl::Vtl0,
1271 VmbusStorageType::Nvme,
1272 )
1273 .add_vmbus_drive(
1274 boot_drive,
1275 &PETRI_NVME_BOOT_VTL0_CONTROLLER,
1276 Some(PETRI_NVME_BOOT_NSID),
1277 ),
1278 BootDeviceType::NvmeViaScsi => todo!(),
1279 BootDeviceType::NvmeViaNvme => todo!(),
1280 BootDeviceType::PcieNvme => {
1281 let port_name = self
1282 .pcie_boot_port
1283 .clone()
1284 .unwrap_or_else(|| "s0rc0rp0".into());
1285 self.config.pcie_nvme_drives.push(PcieNvmeDrive {
1286 port_name,
1287 nsid: 1,
1288 drive: boot_drive,
1289 });
1290 self
1291 }
1292 BootDeviceType::PcieVirtioBlk => {
1293 self.config.pcie_virtio_blk_drives.push(PcieVirtioBlkDrive {
1294 port_name: self
1295 .pcie_boot_port
1296 .clone()
1297 .unwrap_or_else(|| "s0rc0rp0".into()),
1298 drive: boot_drive,
1299 });
1300 self
1301 }
1302 }
1303 } else {
1304 self
1305 };
1306 Ok(builder)
1307 }
1308
1309 fn has_agent_disk(&self) -> bool {
1314 if self.uses_pipette_as_init() {
1315 self.agent_image.as_ref().is_some_and(|i| i.has_extras())
1316 } else {
1317 self.agent_image.is_some()
1318 }
1319 }
1320
1321 pub fn properties(&self) -> PetriVmProperties {
1323 PetriVmProperties {
1324 is_openhcl: self.config.firmware.is_openhcl(),
1325 is_isolated: self.config.firmware.isolation().is_some(),
1326 is_pcat: self.config.firmware.is_pcat(),
1327 is_linux_direct: self.config.firmware.is_linux_direct(),
1328 using_vtl0_pipette: self.using_vtl0_pipette(),
1329 external_pipette: self.external_pipette,
1330 using_vpci: self.boot_device_type.requires_vpci_boot(),
1331 os_flavor: self.config.firmware.os_flavor(),
1332 minimal_mode: self.minimal_mode,
1333 uses_pipette_as_init: self.uses_pipette_as_init(),
1334 enable_serial: self.enable_serial,
1335 use_virtio_console: self.use_virtio_console,
1336 has_agent_disk: self.has_agent_disk(),
1337 use_virtio_vsock: self.use_virtio_vsock,
1338 #[cfg(target_os = "linux")]
1339 vhost_vsock_guest_cid: self.vhost_vsock_guest_cid,
1340 no_vmbus: self.no_vmbus,
1341 no_hv: self.no_hv,
1342 }
1343 }
1344
1345 fn uses_pipette_as_init(&self) -> bool {
1351 self.config.firmware.is_linux_direct()
1352 && !self.config.firmware.is_openhcl()
1353 && self.pipette_binary.is_some()
1354 }
1355
1356 pub fn using_vtl0_pipette(&self) -> bool {
1358 self.external_pipette
1359 || self.uses_pipette_as_init()
1360 || self
1361 .agent_image
1362 .as_ref()
1363 .is_some_and(|x| x.contains_pipette())
1364 }
1365
1366 pub async fn run_without_agent(self) -> anyhow::Result<PetriVm<T>> {
1370 self.run_core().await
1371 }
1372
1373 pub async fn run(self) -> anyhow::Result<(PetriVm<T>, PipetteClient)> {
1376 assert!(self.using_vtl0_pipette());
1377
1378 let mut vm = self.add_nested_artifacts_to_agent_disk().run_core().await?;
1379 let client = vm.wait_for_agent().await?;
1380
1381 Ok((vm, client))
1382 }
1383
1384 async fn run_core(mut self) -> anyhow::Result<PetriVm<T>> {
1385 self = self.add_boot_disk()?.add_agent_disks()?;
1388
1389 if self.uses_pipette_as_init() && self.resources.prebuilt_initrd.is_none() {
1393 self.resources.prebuilt_initrd = Some(self.prepare_initrd()?);
1394 }
1395
1396 tracing::debug!(builder = ?self);
1397
1398 let arch = self.config.arch;
1399 let expect_reset = self.expect_reset();
1400 let properties = self.properties();
1401 let guest_shares = self.config.guest_shares.clone();
1402 let name = self.config.name.clone();
1403
1404 let (mut runtime, config) = self
1405 .backend
1406 .run(
1407 self.config,
1408 self.modify_vmm_config,
1409 &self.resources,
1410 &properties,
1411 )
1412 .await?;
1413 let openhcl_diag_handler = runtime.openhcl_diag();
1414 let watchdog_tasks = Self::start_watchdog_tasks(
1415 &self.resources,
1416 &mut runtime,
1417 self.enable_screenshots,
1418 self.watchdog_timeout,
1419 )?;
1420
1421 let mut vm = PetriVm {
1422 runtime: PetriVmRuntimeGuard {
1423 runtime: Some(runtime),
1424 driver: self.resources.driver.clone(),
1425 log_source: self.resources.log_source.clone(),
1426 capture_inspect_on_drop: self.capture_inspect_on_failure,
1427 },
1428 resources: self.resources,
1429 watchdog_tasks,
1430 openhcl_diag_handler,
1431
1432 arch,
1433 guest_quirks: self.guest_quirks,
1434 vmm_quirks: self.vmm_quirks,
1435 expected_boot_event: self.expected_boot_event,
1436
1437 config,
1438 properties,
1439
1440 nested_test: self.nested_test,
1441 guest_shares,
1442 name,
1443 };
1444
1445 if expect_reset {
1446 vm.wait_for_reset_core().await?;
1447 }
1448
1449 vm.wait_for_expected_boot_event().await?;
1450
1451 Ok(vm)
1452 }
1453
1454 fn expect_reset(&self) -> bool {
1455 self.override_expect_reset
1456 || matches!(
1457 (
1458 self.guest_quirks.initial_reboot,
1459 self.expected_boot_event,
1460 &self.config.firmware,
1461 &self.config.tpm,
1462 ),
1463 (
1464 Some(InitialRebootCondition::Always),
1465 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
1466 _,
1467 _,
1468 ) | (
1469 Some(InitialRebootCondition::WithTpm),
1470 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
1471 _,
1472 Some(_),
1473 )
1474 )
1475 }
1476
1477 fn start_watchdog_timer(
1478 driver: &DefaultDriver,
1479 watchdog_timeout: Option<Duration>,
1480 ) -> Option<Task<()>> {
1481 watchdog_timeout.map(|timeout| {
1482 let timer_driver = driver.clone();
1484 driver.spawn("timer-watchdog", async move {
1485 PolledTimer::new(&timer_driver).sleep(timeout).await;
1486 tracing::error!(?timeout, "Test timeout reached, aborting.");
1487 panic!("Test timed out");
1488 })
1489 })
1490 }
1491
1492 fn start_watchdog_tasks(
1493 resources: &PetriVmResources,
1494 runtime: &mut T::VmRuntime,
1495 enable_screenshots: bool,
1496 watchdog_timeout: Option<Duration>,
1497 ) -> anyhow::Result<Vec<Task<()>>> {
1498 let mut tasks = Vec::new();
1499
1500 tasks.extend(Self::start_watchdog_timer(
1501 &resources.driver,
1502 watchdog_timeout,
1503 ));
1504
1505 if enable_screenshots {
1506 if let Some(mut framebuffer_access) = runtime.take_framebuffer_access() {
1507 let mut timer = PolledTimer::new(&resources.driver);
1508 let log_source = resources.log_source.clone();
1509
1510 tasks.push(
1511 resources
1512 .driver
1513 .spawn("petri-watchdog-screenshot", async move {
1514 let mut image = Vec::new();
1515 let mut last_image = Vec::new();
1516 loop {
1517 timer.sleep(Duration::from_secs(2)).await;
1518 tracing::trace!("Taking screenshot.");
1519
1520 let VmScreenshotMeta {
1521 color,
1522 width,
1523 height,
1524 } = match framebuffer_access.screenshot(&mut image).await {
1525 Ok(Some(meta)) => meta,
1526 Ok(None) => {
1527 tracing::debug!("VM off, skipping screenshot.");
1528 continue;
1529 }
1530 Err(e) => {
1531 tracing::error!(?e, "Failed to take screenshot");
1532 continue;
1533 }
1534 };
1535
1536 if image == last_image {
1537 tracing::debug!(
1538 "No change in framebuffer, skipping screenshot."
1539 );
1540 continue;
1541 }
1542
1543 let r = log_source.create_attachment("screenshot.png").and_then(
1544 |mut f| {
1545 image::write_buffer_with_format(
1546 &mut f,
1547 &image,
1548 width.into(),
1549 height.into(),
1550 color,
1551 image::ImageFormat::Png,
1552 )
1553 .map_err(Into::into)
1554 },
1555 );
1556
1557 if let Err(e) = r {
1558 tracing::error!(?e, "Failed to save screenshot");
1559 } else {
1560 tracing::info!("Screenshot saved.");
1561 }
1562
1563 std::mem::swap(&mut image, &mut last_image);
1564 }
1565 }),
1566 );
1567 }
1568 }
1569
1570 Ok(tasks)
1571 }
1572
1573 pub fn with_expect_boot_failure(mut self) -> Self {
1576 self.expected_boot_event = Some(FirmwareEvent::BootFailed);
1577 self
1578 }
1579
1580 pub fn with_expect_no_boot_event(mut self) -> Self {
1583 self.expected_boot_event = None;
1584 self
1585 }
1586
1587 pub fn with_expect_reset(mut self) -> Self {
1591 self.override_expect_reset = true;
1592 self
1593 }
1594
1595 pub fn with_secure_boot(mut self) -> Self {
1597 self.config
1598 .firmware
1599 .uefi_config_mut()
1600 .expect("Secure boot is only supported for UEFI firmware.")
1601 .secure_boot_enabled = true;
1602
1603 match self.os_flavor() {
1604 OsFlavor::Windows => self.with_windows_secure_boot_template(),
1605 OsFlavor::Linux => self.with_uefi_ca_secure_boot_template(),
1606 _ => panic!(
1607 "Secure boot unsupported for OS flavor {:?}",
1608 self.os_flavor()
1609 ),
1610 }
1611 }
1612
1613 pub fn with_windows_secure_boot_template(mut self) -> Self {
1615 self.config
1616 .firmware
1617 .uefi_config_mut()
1618 .expect("Secure boot is only supported for UEFI firmware.")
1619 .secure_boot_template = Some(SecureBootTemplate::MicrosoftWindows);
1620 self
1621 }
1622
1623 pub fn with_uefi_ca_secure_boot_template(mut self) -> Self {
1625 self.config
1626 .firmware
1627 .uefi_config_mut()
1628 .expect("Secure boot is only supported for UEFI firmware.")
1629 .secure_boot_template = Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority);
1630 self
1631 }
1632
1633 pub fn with_custom_uefi_json(mut self, json: impl Into<Vec<u8>>) -> Self {
1635 self.config
1636 .firmware
1637 .uefi_config_mut()
1638 .expect("Custom UEFI variables are only supported for UEFI firmware.")
1639 .custom_uefi_json = Some(json.into());
1640 self
1641 }
1642
1643 pub fn with_processor_topology(mut self, topology: ProcessorTopology) -> Self {
1645 self.config.proc_topology = topology;
1646 self
1647 }
1648
1649 pub fn with_memory(mut self, memory: MemoryConfig) -> Self {
1651 self.config.memory = memory;
1652 self
1653 }
1654
1655 pub fn with_vtl2_base_address_type(mut self, address_type: Vtl2BaseAddressType) -> Self {
1660 self.config
1661 .firmware
1662 .openhcl_config_mut()
1663 .expect("OpenHCL firmware is required to set custom VTL2 address type.")
1664 .vtl2_base_address_type = Some(address_type);
1665 self
1666 }
1667
1668 pub fn with_custom_openhcl(mut self, artifact: ResolvedArtifact<impl IsOpenhclIgvm>) -> Self {
1670 match &mut self.config.firmware {
1671 Firmware::OpenhclLinuxDirect { igvm_firmware, .. }
1672 | Firmware::OpenhclPcat { igvm_firmware, .. }
1673 | Firmware::OpenhclUefi { igvm_firmware, .. } => {
1674 *igvm_firmware = IgvmFirmwareSource::File(artifact.erase());
1675 }
1676 Firmware::LinuxDirect { .. }
1677 | Firmware::IgvmLinuxDirect { .. }
1678 | Firmware::Uefi { .. }
1679 | Firmware::Pcat { .. } => {
1680 panic!("Custom OpenHCL is only supported for OpenHCL firmware.")
1681 }
1682 }
1683 self
1684 }
1685
1686 pub fn with_openhcl_from_vmgs(mut self) -> Self {
1688 match &mut self.config.firmware {
1689 Firmware::OpenhclLinuxDirect { igvm_firmware, .. }
1690 | Firmware::OpenhclPcat { igvm_firmware, .. }
1691 | Firmware::OpenhclUefi { igvm_firmware, .. } => {
1692 *igvm_firmware = IgvmFirmwareSource::VmgsOrInBox;
1693 }
1694 Firmware::LinuxDirect { .. }
1695 | Firmware::IgvmLinuxDirect { .. }
1696 | Firmware::Uefi { .. }
1697 | Firmware::Pcat { .. } => {
1698 panic!("Loading OpenHCL from VMGS is only supported for OpenHCL firmware.")
1699 }
1700 }
1701 self
1702 }
1703
1704 pub fn with_openhcl_command_line(mut self, additional_command_line: &str) -> Self {
1706 append_cmdline(
1707 &mut self
1708 .config
1709 .firmware
1710 .openhcl_config_mut()
1711 .expect("OpenHCL command line is only supported for OpenHCL firmware.")
1712 .custom_command_line,
1713 additional_command_line,
1714 );
1715 self
1716 }
1717
1718 pub fn with_mana_keepalive(mut self, enable: bool) -> Self {
1720 self.config
1721 .firmware
1722 .openhcl_config_mut()
1723 .expect("MANA keepalive is only supported for OpenHCL firmware.")
1724 .enable_mana_keepalive = enable;
1725 self
1726 }
1727
1728 pub fn with_confidential_filtering(self) -> Self {
1730 if !self.config.firmware.is_openhcl() {
1731 panic!("Confidential filtering is only supported for OpenHCL");
1732 }
1733 self.with_openhcl_command_line(&format!(
1734 "{}=1 {}=0",
1735 underhill_confidentiality::OPENHCL_CONFIDENTIAL_ENV_VAR_NAME,
1736 underhill_confidentiality::OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME
1737 ))
1738 }
1739
1740 pub fn with_openhcl_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1742 self.config
1743 .firmware
1744 .openhcl_config_mut()
1745 .expect("OpenHCL firmware is required to set custom OpenHCL log levels.")
1746 .log_levels = levels;
1747 self
1748 }
1749
1750 pub fn with_host_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1754 if let OpenvmmLogConfig::Custom(ref custom_levels) = levels {
1755 for key in custom_levels.keys() {
1756 if !["OPENVMM_LOG", "OPENVMM_SHOW_SPANS"].contains(&key.as_str()) {
1757 panic!("Unsupported OpenVMM log level key: {}", key);
1758 }
1759 }
1760 }
1761
1762 self.config.host_log_levels = Some(levels.clone());
1763 self
1764 }
1765
1766 pub fn with_agent_file(mut self, name: &str, file_path: impl AsRef<Path>) -> Self {
1768 self.agent_image
1769 .as_mut()
1770 .expect("no guest pipette")
1771 .add_file(name, file_path);
1772 self
1773 }
1774
1775 pub fn with_openhcl_agent_file(mut self, name: &str, file_path: impl AsRef<Path>) -> Self {
1777 self.openhcl_agent_image
1778 .as_mut()
1779 .expect("no openhcl pipette")
1780 .add_file(name, file_path);
1781 self
1782 }
1783
1784 pub fn with_uefi_frontpage(mut self, enable: bool) -> Self {
1786 self.config
1787 .firmware
1788 .uefi_config_mut()
1789 .expect("UEFI frontpage is only supported for UEFI firmware.")
1790 .disable_frontpage = !enable;
1791 self
1792 }
1793
1794 pub fn with_efi_diagnostics_log_level(mut self, level: EfiDiagnosticsLogLevel) -> Self {
1800 self.config
1801 .firmware
1802 .uefi_config_mut()
1803 .expect("EFI diagnostics log level is only supported for UEFI firmware.")
1804 .efi_diagnostics_log_level = level;
1805 self
1806 }
1807
1808 pub fn with_efi_diagnostics_rate_limit(mut self, limit: u32) -> Self {
1814 self.config
1815 .firmware
1816 .uefi_config_mut()
1817 .expect("EFI diagnostics rate limit is only supported for UEFI firmware.")
1818 .efi_diagnostics_rate_limit = Some(limit);
1819 self
1820 }
1821
1822 pub fn with_default_boot_always_attempt(mut self, enable: bool) -> Self {
1824 self.config
1825 .firmware
1826 .uefi_config_mut()
1827 .expect("Default boot always attempt is only supported for UEFI firmware.")
1828 .default_boot_always_attempt = enable;
1829 self
1830 }
1831
1832 pub fn with_uefi_force_dma_bounce(mut self, enable: bool) -> Self {
1834 self.config
1835 .firmware
1836 .uefi_config_mut()
1837 .expect("force DMA bounce is only supported for UEFI firmware.")
1838 .force_dma_bounce = enable;
1839 self
1840 }
1841
1842 pub fn with_vmbus_redirect(mut self, enable: bool) -> Self {
1844 self.config
1845 .firmware
1846 .openhcl_config_mut()
1847 .expect("VMBus redirection is only supported for OpenHCL firmware.")
1848 .vmbus_redirect = enable;
1849 self
1850 }
1851
1852 pub fn with_hibernation_enabled(mut self, enable: bool) -> Self {
1858 self.config.hibernation_enabled = enable;
1859 self
1860 }
1861
1862 pub fn with_ipmi(mut self, enable: bool) -> Self {
1864 self.config.ipmi_enabled = enable;
1865 self
1866 }
1867
1868 pub fn with_guest_state_lifetime(
1870 mut self,
1871 guest_state_lifetime: PetriGuestStateLifetime,
1872 ) -> Self {
1873 let disk = match self.config.vmgs {
1874 PetriVmgsResource::Disk(disk)
1875 | PetriVmgsResource::ReprovisionOnFailure(disk)
1876 | PetriVmgsResource::ReprovisionSingle(disk) => disk,
1877 PetriVmgsResource::Ephemeral => PetriVmgsDisk::default(),
1878 };
1879 self.config.vmgs = match guest_state_lifetime {
1880 PetriGuestStateLifetime::Disk => PetriVmgsResource::Disk(disk),
1881 PetriGuestStateLifetime::ReprovisionOnFailure => {
1882 PetriVmgsResource::ReprovisionOnFailure(disk)
1883 }
1884 PetriGuestStateLifetime::ReprovisionSingle => {
1885 PetriVmgsResource::ReprovisionSingle(disk)
1886 }
1887 PetriGuestStateLifetime::Ephemeral => {
1888 if !matches!(disk.disk, Disk::Memory(_)) {
1889 panic!("attempted to use ephemeral guest state after specifying backing vmgs")
1890 }
1891 PetriVmgsResource::Ephemeral
1892 }
1893 };
1894 self
1895 }
1896
1897 pub fn with_guest_state_encryption(mut self, policy: GuestStateEncryptionPolicy) -> Self {
1899 match &mut self.config.vmgs {
1900 PetriVmgsResource::Disk(vmgs)
1901 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1902 | PetriVmgsResource::ReprovisionSingle(vmgs) => {
1903 vmgs.encryption_policy = policy;
1904 }
1905 PetriVmgsResource::Ephemeral => {
1906 panic!("attempted to encrypt ephemeral guest state")
1907 }
1908 }
1909 self
1910 }
1911
1912 pub fn with_initial_vmgs(self, disk: ResolvedArtifact<impl IsTestVmgs>) -> Self {
1914 self.with_backing_vmgs(Disk::Differencing(DiskPath::Local(disk.into())))
1915 }
1916
1917 pub fn with_persistent_vmgs(self, disk: impl AsRef<Path>) -> Self {
1919 self.with_backing_vmgs(Disk::Persistent(disk.as_ref().to_path_buf()))
1920 }
1921
1922 fn with_backing_vmgs(mut self, disk: Disk) -> Self {
1923 match &mut self.config.vmgs {
1924 PetriVmgsResource::Disk(vmgs)
1925 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1926 | PetriVmgsResource::ReprovisionSingle(vmgs) => {
1927 if !matches!(vmgs.disk, Disk::Memory(_)) {
1928 panic!("already specified a backing vmgs file");
1929 }
1930 vmgs.disk = disk;
1931 }
1932 PetriVmgsResource::Ephemeral => {
1933 panic!("attempted to specify a backing vmgs with ephemeral guest state")
1934 }
1935 }
1936 self
1937 }
1938
1939 pub fn with_boot_device_type(mut self, boot: BootDeviceType) -> Self {
1943 self.boot_device_type = boot;
1944 self
1945 }
1946
1947 pub fn with_pcie_boot_port(mut self, port_name: &str) -> Self {
1953 self.pcie_boot_port = Some(port_name.to_string());
1954 self
1955 }
1956
1957 pub fn with_tpm(mut self, enable: bool) -> Self {
1959 if enable {
1960 self.config.tpm.get_or_insert_default();
1961 } else {
1962 self.config.tpm = None;
1963 }
1964 self
1965 }
1966
1967 pub fn with_tpm_state_persistence(mut self, tpm_state_persistence: bool) -> Self {
1969 self.config
1970 .tpm
1971 .as_mut()
1972 .expect("TPM persistence requires a TPM")
1973 .no_persistent_secrets = !tpm_state_persistence;
1974 self
1975 }
1976
1977 pub fn with_hardware_sealing_policy(mut self, policy: PetriHardwareSealingPolicy) -> Self {
1979 self.config
1980 .tpm
1981 .as_mut()
1982 .expect("hardware sealing policy requires a TPM")
1983 .hardware_sealing_policy = policy;
1984 self
1985 }
1986
1987 pub fn with_tpm_version(mut self, version: PetriTpmVersion) -> Self {
1989 self.config
1990 .tpm
1991 .as_mut()
1992 .expect("TPM version requires a TPM")
1993 .version = version;
1994 self
1995 }
1996
1997 pub fn with_custom_vtl2_settings(
2001 mut self,
2002 f: impl FnOnce(&mut Vtl2Settings) + 'static + Send + Sync,
2003 ) -> Self {
2004 f(self
2005 .config
2006 .firmware
2007 .vtl2_settings()
2008 .expect("Custom VTL 2 settings are only supported with OpenHCL"));
2009 self
2010 }
2011
2012 pub fn add_vtl2_storage_controller(self, controller: StorageController) -> Self {
2014 self.with_custom_vtl2_settings(move |v| {
2015 v.dynamic
2016 .as_mut()
2017 .unwrap()
2018 .storage_controllers
2019 .push(controller)
2020 })
2021 }
2022
2023 pub fn add_vmbus_storage_controller(
2025 mut self,
2026 id: &Guid,
2027 target_vtl: Vtl,
2028 controller_type: VmbusStorageType,
2029 ) -> Self {
2030 if self
2031 .config
2032 .vmbus_storage_controllers
2033 .insert(
2034 *id,
2035 VmbusStorageController::new(target_vtl, controller_type),
2036 )
2037 .is_some()
2038 {
2039 panic!("storage controller {id} already existed");
2040 }
2041 self
2042 }
2043
2044 pub fn add_vmbus_drive(
2046 mut self,
2047 drive: Drive,
2048 controller_id: &Guid,
2049 controller_location: Option<u32>,
2050 ) -> Self {
2051 let controller = self
2052 .config
2053 .vmbus_storage_controllers
2054 .get_mut(controller_id)
2055 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
2056
2057 _ = controller.set_drive(controller_location, drive, false);
2058
2059 self
2060 }
2061
2062 pub fn add_ide_drive(
2064 mut self,
2065 drive: Drive,
2066 controller_number: u32,
2067 controller_location: u8,
2068 ) -> Self {
2069 self.config
2070 .firmware
2071 .ide_controllers_mut()
2072 .expect("Host IDE requires PCAT with no HCL")[controller_number as usize]
2073 [controller_location as usize] = Some(drive);
2074
2075 self
2076 }
2077
2078 pub fn add_physical_nvme_device(mut self, vsid: Guid, device: PhysicalNvmeDevice) -> Self {
2080 if self
2081 .config
2082 .physical_nvme_devices
2083 .insert(vsid, device)
2084 .is_some()
2085 {
2086 panic!("physical NVMe device {vsid} already existed");
2087 }
2088 self
2089 }
2090
2091 pub fn os_flavor(&self) -> OsFlavor {
2093 self.config.firmware.os_flavor()
2094 }
2095
2096 pub fn is_openhcl(&self) -> bool {
2098 self.config.firmware.is_openhcl()
2099 }
2100
2101 pub fn isolation(&self) -> Option<IsolationType> {
2103 self.config.firmware.isolation()
2104 }
2105
2106 pub fn arch(&self) -> MachineArch {
2108 self.config.arch
2109 }
2110
2111 pub fn log_source(&self) -> &PetriLogSource {
2113 &self.resources.log_source
2114 }
2115
2116 pub fn default_servicing_flags(&self) -> OpenHclServicingFlags {
2118 T::default_servicing_flags()
2119 }
2120
2121 pub fn modify_backend(
2124 mut self,
2125 f: impl FnOnce(T::VmmConfig) -> T::VmmConfig + 'static + Send,
2126 ) -> Self {
2127 let previous = self.modify_vmm_config.take();
2128 self.modify_vmm_config = Some(ModifyFn(Box::new(move |config| {
2129 let config = match previous {
2130 Some(previous) => previous.0(config),
2131 None => config,
2132 };
2133 f(config)
2134 })));
2135 self
2136 }
2137
2138 fn add_nested_artifacts_to_agent_disk(mut self) -> Self {
2139 let Some(nested_test) = self.nested_test.as_ref() else {
2140 return self;
2141 };
2142
2143 if T::supports_file_sharing(&self.config.firmware) {
2146 self.config.guest_shares.insert(
2147 NESTED_CONTENT_TAG.into(),
2148 GuestShare {
2149 host_path: PathBuf::from(
2150 std::env::var(VMM_TESTS_CONTENT_DIR).expect("test content dir not set"),
2151 ),
2152 guest_path: self.agent_disk_path(&[NESTED_TEST_CONTENT_DIR.into()]),
2153 },
2154 );
2155 self.config.guest_shares.insert(
2156 NESTED_IMAGES_TAG.into(),
2157 GuestShare {
2158 host_path: PathBuf::from(
2159 std::env::var(VMM_TEST_IMAGES).expect("test images dir not set"),
2160 ),
2161 guest_path: self.agent_disk_path(&[NESTED_IMAGES_DIR.into()]),
2162 },
2163 );
2164
2165 return self;
2166 }
2167
2168 let mut extras = Vec::new();
2169
2170 let path_builder = FatFsPathBuilder(self.os_flavor());
2171
2172 extras.push((NESTED_TEST_CONTENT_DIR.to_string(), ImageEntry::Dir));
2173 extras.push((
2174 path_builder
2175 .build_guest_path(&[NESTED_TEST_CONTENT_DIR.into(), NESTED_RESULTS_DIR.into()]),
2176 ImageEntry::Dir,
2177 ));
2178 extras.push((
2179 path_builder.build_guest_path(&[NESTED_IMAGES_DIR.into()]),
2180 ImageEntry::Dir,
2181 ));
2182 extras.push((
2183 path_builder.build_guest_path(&[
2184 NESTED_TEST_CONTENT_DIR.into(),
2185 path_builder.binary_with_extension(NESTED_TEST_BINARY),
2186 ]),
2187 ImageEntry::File(nested_test.test_binary.to_path_buf()),
2188 ));
2189
2190 for (handle, src) in &nested_test.artifacts {
2191 let relative_path = path_builder.convert_guest_path(&handle.relative_path());
2192 let dest = if handle.is_image() {
2193 path_builder.build_guest_path(&[NESTED_IMAGES_DIR.into(), relative_path])
2194 } else {
2195 path_builder.build_guest_path(&[NESTED_TEST_CONTENT_DIR.into(), relative_path])
2196 };
2197
2198 for parent in path_builder.parents(&dest) {
2199 if !extras.iter().any(|(p, _)| p == parent) {
2200 extras.push((parent.to_string(), ImageEntry::Dir))
2201 }
2202 }
2203
2204 extras.push((dest, ImageEntry::File(src.clone())));
2205 }
2206
2207 let image = self.agent_image.as_mut().expect("no guest pipette");
2208 image.add_extras(extras);
2209 image.allocate_free_space(150 * 1024 * 1024);
2211
2212 self
2213 }
2214
2215 pub fn with_nested_virt(mut self) -> Self {
2217 self.config.nested_virt_enabled = true;
2218 self
2219 }
2220
2221 pub fn with_custom_linux_kernel(
2223 mut self,
2224 custom_kernel: ResolvedArtifact<impl IsLoadable>,
2225 ) -> Self {
2226 let Firmware::LinuxDirect { kernel, .. } = &mut self.config.firmware else {
2227 panic!("VM is not linux direct");
2228 };
2229 *kernel = custom_kernel.erase();
2230 self
2231 }
2232}
2233
2234impl<T: PetriVmmBackend> PetriVm<T> {
2235 pub async fn teardown(mut self) -> anyhow::Result<()> {
2237 tracing::info!("Tearing down VM...");
2238 self.runtime.take_for_teardown().teardown().await?;
2239 Ok(())
2240 }
2241
2242 pub async fn wait_for_halt(&mut self) -> anyhow::Result<PetriHaltReasonDetail> {
2244 tracing::info!("Waiting for VM to halt...");
2245 let halt_reason = self.runtime.wait_for_halt(false).await?;
2246 tracing::info!("VM halted: {halt_reason:?}. Cancelling watchdogs...");
2247 futures::future::join_all(self.watchdog_tasks.drain(..).map(|t| t.cancel())).await;
2248 Ok(halt_reason)
2249 }
2250
2251 pub async fn wait_for_clean_shutdown(&mut self) -> anyhow::Result<()> {
2253 let halt_reason = self.wait_for_halt().await?;
2254 if halt_reason.reason != PetriHaltReason::PowerOff {
2255 anyhow::bail!("Expected PowerOff, got {halt_reason:?}");
2256 }
2257 tracing::info!("VM was cleanly powered off and torn down.");
2258 Ok(())
2259 }
2260
2261 pub async fn wait_for_teardown(mut self) -> anyhow::Result<PetriHaltReasonDetail> {
2264 let halt_reason = self.wait_for_halt().await?;
2265 self.teardown().await?;
2266 Ok(halt_reason)
2267 }
2268
2269 pub async fn wait_for_clean_teardown(mut self) -> anyhow::Result<()> {
2271 self.wait_for_clean_shutdown().await?;
2272 self.teardown().await
2273 }
2274
2275 pub async fn wait_for_reset_no_agent(&mut self) -> anyhow::Result<()> {
2277 self.wait_for_reset_core().await?;
2278 self.wait_for_expected_boot_event().await?;
2279 Ok(())
2280 }
2281
2282 pub async fn wait_for_reset(&mut self) -> anyhow::Result<PipetteClient> {
2284 self.wait_for_reset_no_agent().await?;
2285 self.wait_for_agent().await
2286 }
2287
2288 async fn wait_for_reset_core(&mut self) -> anyhow::Result<()> {
2289 tracing::info!("Waiting for VM to reset...");
2290 let halt_reason = self.runtime.wait_for_halt(true).await?;
2291 if halt_reason.reason != PetriHaltReason::Reset {
2292 anyhow::bail!("Expected reset, got {halt_reason:?}");
2293 }
2294 tracing::info!("VM reset.");
2295 Ok(())
2296 }
2297
2298 pub async fn inspect_openhcl(
2309 &self,
2310 path: impl Into<String>,
2311 depth: Option<usize>,
2312 timeout: Option<Duration>,
2313 ) -> anyhow::Result<inspect::Node> {
2314 self.openhcl_diag()?
2315 .inspect(path.into().as_str(), depth, timeout)
2316 .await
2317 }
2318
2319 pub async fn inspect_update_openhcl(
2329 &self,
2330 path: impl Into<String>,
2331 value: impl Into<String>,
2332 ) -> anyhow::Result<inspect::Value> {
2333 self.openhcl_diag()?
2334 .inspect_update(path.into(), value.into())
2335 .await
2336 }
2337
2338 pub async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()> {
2340 self.inspect_openhcl("", None, None).await.map(|_| ())
2341 }
2342
2343 pub async fn dump_openhcl_saved_state(&self) -> anyhow::Result<Vec<u8>> {
2347 self.openhcl_diag()?.dump_saved_state().await
2348 }
2349
2350 pub async fn inspect_vmm(&self, path: &str) -> anyhow::Result<inspect::Node> {
2361 use anyhow::Context;
2362
2363 let inspector = self
2364 .runtime
2365 .inspector()
2366 .context("this VMM backend does not support inspect")?;
2367 inspector.inspect(path).await
2368 }
2369
2370 pub async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()> {
2376 self.openhcl_diag()?.wait_for_vtl2().await
2377 }
2378
2379 pub async fn kmsg(&self) -> anyhow::Result<diag_client::kmsg_stream::KmsgStream> {
2381 self.openhcl_diag()?.kmsg().await
2382 }
2383
2384 pub async fn openhcl_core_dump(&self, name: &str, path: &Path) -> anyhow::Result<()> {
2387 self.openhcl_diag()?.core_dump(name, path).await
2388 }
2389
2390 pub async fn openhcl_crash(&self, name: &str) -> anyhow::Result<()> {
2392 self.openhcl_diag()?.crash(name).await
2393 }
2394
2395 async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient> {
2398 self.runtime.wait_for_enlightened_shutdown_ready().await?;
2408 let client = self.runtime.wait_for_agent(false).await?;
2409 self.mount_guest_shares(&client).await?;
2410 Ok(client)
2411 }
2412
2413 async fn mount_guest_shares(&self, client: &PipetteClient) -> anyhow::Result<()> {
2414 if !self.guest_shares.is_empty() {
2415 anyhow::ensure!(
2416 self.properties.os_flavor == OsFlavor::Linux,
2417 "directory sharing requires a Linux guest"
2418 );
2419 let mut shares = self.guest_shares.iter().collect::<Vec<_>>();
2420 shares.sort_by_key(|(tag, share)| {
2422 (
2423 share
2424 .guest_path
2425 .split('/')
2426 .filter(|part| !part.is_empty() && *part != ".")
2427 .count(),
2428 *tag,
2429 )
2430 });
2431 for (tag, share) in shares {
2432 let shell = client.unix_shell();
2433 shell
2434 .cmd("mkdir")
2435 .args(["-p", &share.guest_path])
2436 .run()
2437 .await?;
2438 shell
2439 .cmd("mount")
2440 .args([
2441 "-t",
2442 "9p",
2443 "-o",
2444 "trans=virtio,version=9p2000.L",
2445 tag,
2446 &share.guest_path,
2447 ])
2448 .run()
2449 .await
2450 .with_context(|| format!("mounting guest share {tag}"))?;
2451 }
2452 }
2453 Ok(())
2454 }
2455
2456 pub async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient> {
2460 self.launch_vtl2_pipette().await?;
2462 self.runtime.wait_for_agent(true).await
2463 }
2464
2465 async fn wait_for_expected_boot_event(&mut self) -> anyhow::Result<()> {
2472 if let Some(expected_event) = self.expected_boot_event {
2473 let event = self.wait_for_boot_event().await?;
2474
2475 anyhow::ensure!(
2476 event == expected_event,
2477 "Did not receive expected boot event"
2478 );
2479 } else {
2480 tracing::warn!("Boot event not emitted for configured firmware or manually ignored.");
2481 }
2482
2483 Ok(())
2484 }
2485
2486 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
2489 tracing::info!("Waiting for boot event...");
2490 let boot_event = loop {
2491 if let Some(event) = self
2492 .runtime
2493 .wait_for_boot_event(self.vmm_quirks.flaky_boot)
2494 .await?
2495 {
2496 break event;
2497 }
2498
2499 tracing::error!("Did not get boot event in required time, resetting...");
2500 if let Some(inspector) = self.runtime.inspector() {
2501 collect_inspect(
2502 "vmm",
2503 Box::pin(async move { inspector.inspect("").await }),
2504 &self.resources.log_source,
2505 "timeout",
2506 )
2507 .await;
2508 }
2509
2510 self.runtime.reset().await?;
2511 };
2512 tracing::info!("Got boot event: {boot_event:?}");
2513 Ok(boot_event)
2514 }
2515
2516 pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
2519 tracing::info!("Waiting for enlightened shutdown to be ready");
2520 self.runtime.wait_for_enlightened_shutdown_ready().await?;
2521
2522 let mut wait_time = Duration::from_secs(10);
2528
2529 if let Some(duration) = self.guest_quirks.hyperv_shutdown_ic_sleep {
2531 wait_time += duration;
2532 }
2533
2534 tracing::info!(
2535 "Shutdown IC reported ready, waiting for an extra {}s",
2536 wait_time.as_secs()
2537 );
2538 PolledTimer::new(&self.resources.driver)
2539 .sleep(wait_time)
2540 .await;
2541
2542 tracing::info!("Sending enlightened shutdown command");
2543 self.runtime.send_enlightened_shutdown(kind).await
2544 }
2545
2546 pub async fn restart_openhcl(
2549 &mut self,
2550 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
2551 flags: OpenHclServicingFlags,
2552 ) -> anyhow::Result<()> {
2553 self.runtime
2554 .restart_openhcl(&new_openhcl.erase(), flags)
2555 .await
2556 }
2557
2558 pub async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()> {
2561 self.runtime.update_command_line(command_line).await
2562 }
2563
2564 pub async fn add_pcie_device(
2566 &mut self,
2567 port_name: String,
2568 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
2569 ) -> anyhow::Result<()> {
2570 self.runtime.add_pcie_device(port_name, resource).await
2571 }
2572
2573 pub async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
2575 self.runtime.remove_pcie_device(port_name).await
2576 }
2577
2578 pub async fn save_openhcl(
2581 &mut self,
2582 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
2583 flags: OpenHclServicingFlags,
2584 ) -> anyhow::Result<()> {
2585 self.runtime.save_openhcl(&new_openhcl.erase(), flags).await
2586 }
2587
2588 pub async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
2591 self.runtime.restore_openhcl().await
2592 }
2593
2594 pub fn arch(&self) -> MachineArch {
2596 self.arch
2597 }
2598
2599 pub fn backend(&mut self) -> &mut T::VmRuntime {
2601 &mut self.runtime
2602 }
2603
2604 async fn launch_vtl2_pipette(&self) -> anyhow::Result<()> {
2605 tracing::debug!("Launching VTL 2 pipette...");
2606
2607 let res = self
2609 .openhcl_diag()?
2610 .run_vtl2_command("sh", &["-c", "mkdir /cidata && mount LABEL=cidata /cidata"])
2611 .await?;
2612
2613 if !res.exit_status.success() {
2614 anyhow::bail!("Failed to mount VTL 2 pipette drive: {:?}", res);
2615 }
2616
2617 let res = self
2618 .openhcl_diag()?
2619 .run_detached_vtl2_command("sh", &["-c", "/cidata/pipette 2>&1 | logger &"])
2620 .await?;
2621
2622 if !res.success() {
2623 anyhow::bail!("Failed to spawn VTL 2 pipette: {:?}", res);
2624 }
2625
2626 Ok(())
2627 }
2628
2629 fn openhcl_diag(&self) -> anyhow::Result<&OpenHclDiagHandler> {
2630 if let Some(ohd) = self.openhcl_diag_handler.as_ref() {
2631 Ok(ohd)
2632 } else {
2633 anyhow::bail!("VM is not configured with OpenHCL")
2634 }
2635 }
2636
2637 pub async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2639 self.runtime.get_guest_state_file().await
2640 }
2641
2642 pub async fn modify_vtl2_settings(
2644 &mut self,
2645 f: impl FnOnce(&mut Vtl2Settings),
2646 ) -> anyhow::Result<()> {
2647 if self.openhcl_diag_handler.is_none() {
2648 panic!("Custom VTL 2 settings are only supported with OpenHCL");
2649 }
2650 f(self
2651 .config
2652 .vtl2_settings
2653 .get_or_insert_with(default_vtl2_settings));
2654 self.runtime
2655 .set_vtl2_settings(self.config.vtl2_settings.as_ref().unwrap())
2656 .await
2657 }
2658
2659 pub fn get_vmbus_storage_controllers(&self) -> &HashMap<Guid, VmbusStorageController> {
2661 &self.config.vmbus_storage_controllers
2662 }
2663
2664 pub async fn set_vmbus_drive(
2666 &mut self,
2667 drive: Drive,
2668 controller_id: &Guid,
2669 controller_location: Option<u32>,
2670 ) -> anyhow::Result<()> {
2671 let controller = self
2672 .config
2673 .vmbus_storage_controllers
2674 .get_mut(controller_id)
2675 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
2676
2677 let controller_location = controller.set_drive(controller_location, drive, true);
2678 let disk = controller.drives.get(&controller_location).unwrap();
2679
2680 self.runtime
2681 .set_vmbus_drive(disk, controller_id, controller_location)
2682 .await?;
2683
2684 Ok(())
2685 }
2686
2687 pub async fn run_nested_test(
2689 &self,
2690 client: &PipetteClient,
2691 env: BTreeMap<String, String>,
2692 ) -> anyhow::Result<()> {
2693 let nested_test = self.nested_test.as_ref().context("no nested test")?;
2694
2695 let test_binary_path =
2696 if let Some(test_content_dir) = self.guest_shares.get(NESTED_CONTENT_TAG) {
2697 let test_content_dir = absolute(&test_content_dir.host_path)?;
2698 let local_test_binary_path = absolute(&nested_test.test_binary)?;
2699
2700 self.convert_guest_path(
2701 local_test_binary_path
2702 .strip_prefix(&test_content_dir)
2703 .context("test binary should be under content dir")?,
2704 )
2705 } else {
2706 self.binary_with_extension(NESTED_TEST_BINARY)
2707 };
2708
2709 let mut cmd = client
2710 .command(self.agent_disk_path(&[NESTED_TEST_CONTENT_DIR.into(), test_binary_path]));
2711 cmd.arg("--ignored")
2712 .arg("--exact")
2713 .arg(&nested_test.test_name)
2714 .env(
2715 VMM_TESTS_CONTENT_DIR,
2716 self.agent_disk_path(&[NESTED_TEST_CONTENT_DIR.into()]),
2717 )
2718 .env(
2719 VMM_TEST_IMAGES,
2720 self.agent_disk_path(&[NESTED_IMAGES_DIR.into()]),
2721 )
2722 .env(
2723 TEST_OUTPUT_PATH,
2724 self.agent_disk_path(&[NESTED_TEST_CONTENT_DIR.into(), NESTED_RESULTS_DIR.into()]),
2725 )
2726 .stdout(crate::pipette::process::Stdio::piped())
2727 .stderr(crate::pipette::process::Stdio::piped());
2728
2729 for (k, v) in env {
2730 cmd.env(k, v);
2731 }
2732
2733 if self.guest_shares.contains_key(NESTED_CONTENT_TAG) {
2734 cmd.env(PETRI_NESTED_TEST_PARENT, &self.name);
2735 }
2736
2737 if let Ok(v) = std::env::var(PETRI_REMOTE_ARTIFACTS) {
2738 cmd.env(PETRI_REMOTE_ARTIFACTS, v);
2739 }
2740
2741 let mut child = cmd.spawn().await?;
2742
2743 self.resources
2745 .driver
2746 .spawn(
2747 "log_nested",
2748 crate::log_task(
2749 self.resources.log_source.log_file("stdout")?,
2750 child.stdout.take().unwrap(),
2751 "nested stdout",
2752 ),
2753 )
2754 .detach();
2755
2756 let output = child.wait_with_output().await?;
2757
2758 if !self.guest_shares.contains_key(NESTED_CONTENT_TAG) {
2760 let test_results_dir = self.agent_disk_path(&[
2761 NESTED_TEST_CONTENT_DIR.into(),
2762 NESTED_RESULTS_DIR.into(),
2763 nested_test.test_name.replace("::", "__"),
2764 ]);
2765
2766 for file in client.list_dir(&test_results_dir).await? {
2767 let data = client
2768 .read_file(self.build_guest_path(&[test_results_dir.clone(), file.clone()]))
2769 .await?;
2770 let mut output = self
2771 .resources
2772 .log_source
2773 .create_attachment_no_prefix(&file)?;
2774 output.write_all(&data)?;
2775 }
2776 }
2777
2778 if !output.status.success() {
2779 anyhow::bail!(
2780 "nested test exited with status {}: {}",
2781 output.status,
2782 String::from_utf8_lossy(&output.stderr)
2783 );
2784 }
2785
2786 Ok(())
2787 }
2788}
2789
2790#[async_trait]
2792pub trait PetriVmRuntime: Send + Sync + 'static {
2793 type VmInspector: PetriVmInspector;
2795 type VmFramebufferAccess: PetriVmFramebufferAccess;
2797
2798 async fn teardown(self) -> anyhow::Result<()>;
2800 async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReasonDetail>;
2803 async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>;
2805 fn openhcl_diag(&self) -> Option<OpenHclDiagHandler>;
2807 async fn wait_for_boot_event(
2810 &mut self,
2811 timeout: Option<Duration>,
2812 ) -> anyhow::Result<Option<FirmwareEvent>>;
2813 async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()>;
2816 async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
2818 async fn restart_openhcl(
2821 &mut self,
2822 new_openhcl: &ResolvedArtifact,
2823 flags: OpenHclServicingFlags,
2824 ) -> anyhow::Result<()>;
2825 async fn save_openhcl(
2829 &mut self,
2830 new_openhcl: &ResolvedArtifact,
2831 flags: OpenHclServicingFlags,
2832 ) -> anyhow::Result<()>;
2833 async fn restore_openhcl(&mut self) -> anyhow::Result<()>;
2836 async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()>;
2839 fn inspector(&self) -> Option<Self::VmInspector> {
2841 None
2842 }
2843 fn take_framebuffer_access(&mut self) -> Option<Self::VmFramebufferAccess> {
2846 None
2847 }
2848 async fn reset(&mut self) -> anyhow::Result<()>;
2850 async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
2852 Ok(None)
2853 }
2854 async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()>;
2856 async fn set_vmbus_drive(
2858 &mut self,
2859 disk: &Drive,
2860 controller_id: &Guid,
2861 controller_location: u32,
2862 ) -> anyhow::Result<()>;
2863 async fn add_pcie_device(
2865 &mut self,
2866 port_name: String,
2867 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
2868 ) -> anyhow::Result<()> {
2869 let _ = (port_name, resource);
2870 anyhow::bail!("PCIe hotplug not supported by this backend")
2871 }
2872 async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
2874 let _ = port_name;
2875 anyhow::bail!("PCIe hotplug not supported by this backend")
2876 }
2877}
2878
2879#[async_trait]
2881pub trait PetriVmInspector: Send + Sync + 'static {
2882 async fn inspect(&self, path: &str) -> anyhow::Result<inspect::Node>;
2885}
2886
2887pub struct NoPetriVmInspector;
2889#[async_trait]
2890impl PetriVmInspector for NoPetriVmInspector {
2891 async fn inspect(&self, _path: &str) -> anyhow::Result<inspect::Node> {
2892 unreachable!()
2893 }
2894}
2895
2896pub struct VmScreenshotMeta {
2898 pub color: image::ExtendedColorType,
2900 pub width: u16,
2902 pub height: u16,
2904}
2905
2906#[async_trait]
2908pub trait PetriVmFramebufferAccess: Send + 'static {
2909 async fn screenshot(&mut self, image: &mut Vec<u8>)
2912 -> anyhow::Result<Option<VmScreenshotMeta>>;
2913}
2914
2915pub struct NoPetriVmFramebufferAccess;
2917#[async_trait]
2918impl PetriVmFramebufferAccess for NoPetriVmFramebufferAccess {
2919 async fn screenshot(
2920 &mut self,
2921 _image: &mut Vec<u8>,
2922 ) -> anyhow::Result<Option<VmScreenshotMeta>> {
2923 unreachable!()
2924 }
2925}
2926
2927#[derive(Debug, Clone)]
2929pub struct ProcessorTopology {
2930 pub vp_count: u32,
2932 pub enable_smt: Option<bool>,
2934 pub vps_per_socket: Option<u32>,
2936 pub apic_mode: Option<ApicMode>,
2938}
2939
2940impl Default for ProcessorTopology {
2941 fn default() -> Self {
2942 Self {
2943 vp_count: 2,
2944 enable_smt: None,
2945 vps_per_socket: None,
2946 apic_mode: None,
2947 }
2948 }
2949}
2950
2951impl ProcessorTopology {
2952 pub fn heavy() -> Self {
2954 Self {
2955 vp_count: 16,
2956 vps_per_socket: Some(8),
2957 ..Default::default()
2958 }
2959 }
2960
2961 pub fn very_heavy() -> Self {
2963 Self {
2964 vp_count: 32,
2965 vps_per_socket: Some(16),
2966 ..Default::default()
2967 }
2968 }
2969}
2970
2971#[derive(Debug, Clone, Copy)]
2973pub enum ApicMode {
2974 Xapic,
2976 X2apicSupported,
2978 X2apicEnabled,
2980}
2981
2982#[derive(Debug, Clone)]
2984pub struct MemoryConfig {
2985 pub startup_bytes: u64,
2988 pub dynamic_memory_range: Option<(u64, u64)>,
2992 pub numa_mem_sizes: Option<Vec<u64>>,
2995 pub private_memory: Option<bool>,
3014 pub transparent_hugepages: bool,
3026}
3027
3028impl Default for MemoryConfig {
3029 fn default() -> Self {
3030 Self {
3031 startup_bytes: 4 * 1024 * 1024 * 1024, dynamic_memory_range: None,
3033 numa_mem_sizes: None,
3034 private_memory: None,
3035 transparent_hugepages: true,
3036 }
3037 }
3038}
3039
3040#[derive(Debug, Clone)]
3042pub struct UefiConfig {
3043 pub secure_boot_enabled: bool,
3045 pub secure_boot_template: Option<SecureBootTemplate>,
3047 pub custom_uefi_json: Option<Vec<u8>>,
3049 pub disable_frontpage: bool,
3051 pub default_boot_always_attempt: bool,
3053 pub enable_vpci_boot: bool,
3055 pub force_dma_bounce: bool,
3057 pub efi_diagnostics_log_level: EfiDiagnosticsLogLevel,
3059 pub efi_diagnostics_rate_limit: Option<u32>,
3062}
3063
3064impl Default for UefiConfig {
3065 fn default() -> Self {
3066 Self {
3067 secure_boot_enabled: false,
3068 secure_boot_template: None,
3069 custom_uefi_json: None,
3070 disable_frontpage: true,
3071 default_boot_always_attempt: false,
3072 enable_vpci_boot: false,
3073 force_dma_bounce: false,
3074 efi_diagnostics_log_level: EfiDiagnosticsLogLevel::Default,
3075 efi_diagnostics_rate_limit: None,
3076 }
3077 }
3078}
3079
3080#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
3085pub enum EfiDiagnosticsLogLevel {
3086 #[default]
3088 Default,
3089 Info,
3091 Full,
3093}
3094
3095#[derive(Debug, Clone)]
3097pub enum OpenvmmLogConfig {
3098 TestDefault,
3102 BuiltInDefault,
3105 Custom(BTreeMap<String, String>),
3115}
3116
3117#[derive(Debug, Clone)]
3119pub struct OpenHclConfig {
3120 pub vmbus_redirect: bool,
3122 pub enable_mana_keepalive: bool,
3124 pub custom_command_line: Option<String>,
3128 pub log_levels: OpenvmmLogConfig,
3132 pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
3135 pub vtl2_settings: Option<Vtl2Settings>,
3137}
3138
3139impl OpenHclConfig {
3140 pub fn command_line(&self) -> String {
3143 let mut cmdline = self.custom_command_line.clone();
3144
3145 if self.enable_mana_keepalive {
3146 append_cmdline(&mut cmdline, "OPENHCL_MANA_KEEP_ALIVE=host,privatepool");
3147 }
3148
3149 match &self.log_levels {
3150 OpenvmmLogConfig::TestDefault => {
3151 let default_log_levels = {
3152 let openhcl_tracing = if let Ok(x) =
3154 std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG"))
3155 {
3156 format!("OPENVMM_LOG={x}")
3157 } else {
3158 "OPENVMM_LOG=debug".to_owned()
3159 };
3160 let openhcl_show_spans = if let Ok(x) = std::env::var("OPENVMM_SHOW_SPANS") {
3161 format!("OPENVMM_SHOW_SPANS={x}")
3162 } else {
3163 "OPENVMM_SHOW_SPANS=false".to_owned()
3164 };
3165 format!("{openhcl_tracing} {openhcl_show_spans}")
3166 };
3167 append_cmdline(&mut cmdline, &default_log_levels);
3168 }
3169 OpenvmmLogConfig::BuiltInDefault => {
3170 }
3172 OpenvmmLogConfig::Custom(levels) => {
3173 levels.iter().for_each(|(key, value)| {
3174 append_cmdline(&mut cmdline, format!("{key}={value}"));
3175 });
3176 }
3177 }
3178
3179 cmdline.unwrap_or_default()
3180 }
3181}
3182
3183impl Default for OpenHclConfig {
3184 fn default() -> Self {
3185 Self {
3186 vmbus_redirect: false,
3187 enable_mana_keepalive: true,
3188 custom_command_line: None,
3189 log_levels: OpenvmmLogConfig::TestDefault,
3190 vtl2_base_address_type: None,
3191 vtl2_settings: None,
3192 }
3193 }
3194}
3195
3196#[derive(Debug, Clone)]
3198pub struct TpmConfig {
3199 pub no_persistent_secrets: bool,
3201 pub hardware_sealing_policy: PetriHardwareSealingPolicy,
3203 pub version: PetriTpmVersion,
3205}
3206
3207impl Default for TpmConfig {
3208 fn default() -> Self {
3209 Self {
3210 no_persistent_secrets: true,
3211 hardware_sealing_policy: PetriHardwareSealingPolicy::Default,
3212 version: PetriTpmVersion::default(),
3213 }
3214 }
3215}
3216
3217#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
3219pub enum PetriTpmVersion {
3220 V138,
3222 #[default]
3224 V185,
3225}
3226
3227impl From<PetriTpmVersion> for tpm_resources::TpmVersion {
3228 fn from(version: PetriTpmVersion) -> Self {
3229 match version {
3230 PetriTpmVersion::V138 => tpm_resources::TpmVersion::V138,
3231 PetriTpmVersion::V185 => tpm_resources::TpmVersion::V185,
3232 }
3233 }
3234}
3235
3236impl From<PetriTpmVersion> for get_resources::ged::GedTpmVersion {
3237 fn from(version: PetriTpmVersion) -> Self {
3238 match version {
3239 PetriTpmVersion::V138 => get_resources::ged::GedTpmVersion::V138,
3240 PetriTpmVersion::V185 => get_resources::ged::GedTpmVersion::V185,
3241 }
3242 }
3243}
3244
3245#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
3250pub enum PetriHardwareSealingPolicy {
3251 #[default]
3253 Default,
3254 HashPolicy,
3256 SignerPolicy,
3258}
3259
3260#[derive(Debug, Clone)]
3262pub enum IgvmFirmwareSource {
3263 File(ResolvedArtifact),
3265 VmgsOrInBox,
3267}
3268
3269#[derive(Debug, Clone)]
3273pub enum Firmware {
3274 LinuxDirect {
3276 kernel: ResolvedArtifact,
3278 initrd: ResolvedArtifact,
3280 },
3281 IgvmLinuxDirect {
3283 igvm_path: ResolvedArtifact,
3285 isolation: IsolationType,
3287 },
3288 OpenhclLinuxDirect {
3290 igvm_firmware: IgvmFirmwareSource,
3292 openhcl_config: OpenHclConfig,
3294 },
3295 Pcat {
3297 guest: PcatGuest,
3299 bios_firmware: ResolvedOptionalArtifact,
3301 svga_firmware: ResolvedOptionalArtifact,
3303 ide_controllers: [[Option<Drive>; 2]; 2],
3305 },
3306 OpenhclPcat {
3308 guest: PcatGuest,
3310 igvm_firmware: IgvmFirmwareSource,
3312 bios_firmware: ResolvedOptionalArtifact,
3314 svga_firmware: ResolvedOptionalArtifact,
3316 openhcl_config: OpenHclConfig,
3318 },
3319 Uefi {
3321 guest: UefiGuest,
3323 uefi_firmware: ResolvedArtifact,
3325 uefi_config: UefiConfig,
3327 },
3328 OpenhclUefi {
3330 guest: UefiGuest,
3332 isolation: Option<IsolationType>,
3334 igvm_firmware: IgvmFirmwareSource,
3336 uefi_config: UefiConfig,
3338 openhcl_config: OpenHclConfig,
3340 },
3341}
3342
3343#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3345pub enum BootDeviceType {
3346 None,
3348 Ide,
3350 IdeViaScsi,
3352 IdeViaNvme,
3354 Scsi,
3356 ScsiViaScsi,
3358 ScsiViaNvme,
3360 Nvme,
3362 NvmeViaScsi,
3364 NvmeViaNvme,
3366 PcieNvme,
3368 PcieVirtioBlk,
3370}
3371
3372impl BootDeviceType {
3373 fn requires_vtl2(&self) -> bool {
3374 match self {
3375 BootDeviceType::None
3376 | BootDeviceType::Ide
3377 | BootDeviceType::Scsi
3378 | BootDeviceType::Nvme
3379 | BootDeviceType::PcieNvme
3380 | BootDeviceType::PcieVirtioBlk => false,
3381 BootDeviceType::IdeViaScsi
3382 | BootDeviceType::IdeViaNvme
3383 | BootDeviceType::ScsiViaScsi
3384 | BootDeviceType::ScsiViaNvme
3385 | BootDeviceType::NvmeViaScsi
3386 | BootDeviceType::NvmeViaNvme => true,
3387 }
3388 }
3389
3390 fn requires_vpci_boot(&self) -> bool {
3391 matches!(
3392 self,
3393 BootDeviceType::Nvme | BootDeviceType::NvmeViaScsi | BootDeviceType::NvmeViaNvme
3394 )
3395 }
3396
3397 fn requires_vmbus(&self) -> bool {
3398 match self {
3399 BootDeviceType::None
3400 | BootDeviceType::Ide
3401 | BootDeviceType::PcieNvme
3402 | BootDeviceType::PcieVirtioBlk => false,
3403 BootDeviceType::IdeViaScsi
3404 | BootDeviceType::IdeViaNvme
3405 | BootDeviceType::Scsi
3406 | BootDeviceType::ScsiViaScsi
3407 | BootDeviceType::ScsiViaNvme
3408 | BootDeviceType::Nvme
3409 | BootDeviceType::NvmeViaScsi
3410 | BootDeviceType::NvmeViaNvme => true,
3411 }
3412 }
3413}
3414
3415impl Firmware {
3416 pub fn linux_direct(
3419 resolver: &ArtifactResolver<'_>,
3420 arch: MachineArch,
3421 isolation: Option<IsolationType>,
3422 ) -> Self {
3423 use petri_artifacts_vmm_test::artifacts::loadable::*;
3424 use petri_artifacts_vmm_test::artifacts::snp_igvm::SNP_LINUX_DIRECT_IGVM_X64;
3425 match (arch, isolation) {
3426 (MachineArch::X86_64, None) => Firmware::LinuxDirect {
3427 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_X64).erase(),
3428 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
3429 },
3430 (MachineArch::Aarch64, None) => Firmware::LinuxDirect {
3431 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_AARCH64).erase(),
3432 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_AARCH64).erase(),
3433 },
3434 (MachineArch::X86_64, Some(IsolationType::Snp)) => Firmware::IgvmLinuxDirect {
3435 igvm_path: resolver.require(SNP_LINUX_DIRECT_IGVM_X64).erase(),
3436 isolation: IsolationType::Snp,
3437 },
3438 (arch, Some(isolation)) => {
3439 panic!("Linux direct does not support {isolation:?} isolation on {arch:?}")
3440 }
3441 }
3442 }
3443
3444 pub fn linux_direct_bzimage(resolver: &ArtifactResolver<'_>) -> Self {
3449 use petri_artifacts_vmm_test::artifacts::loadable::*;
3450 Firmware::LinuxDirect {
3451 kernel: resolver.require(LINUX_DIRECT_TEST_BZIMAGE_X64).erase(),
3452 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
3453 }
3454 }
3455
3456 pub fn openhcl_linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
3458 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
3459 match arch {
3460 MachineArch::X86_64 => Firmware::OpenhclLinuxDirect {
3461 igvm_firmware: IgvmFirmwareSource::File(
3462 resolver.require(LATEST_LINUX_DIRECT_TEST_X64).erase(),
3463 ),
3464 openhcl_config: Default::default(),
3465 },
3466 MachineArch::Aarch64 => todo!("Linux direct not yet supported on aarch64"),
3467 }
3468 }
3469
3470 pub fn pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
3472 use petri_artifacts_vmm_test::artifacts::loadable::*;
3473 Firmware::Pcat {
3474 guest,
3475 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
3476 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
3477 ide_controllers: [[None, None], [None, None]],
3478 }
3479 }
3480
3481 pub fn openhcl_pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
3483 use petri_artifacts_vmm_test::artifacts::loadable::*;
3484 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
3485 Firmware::OpenhclPcat {
3486 guest,
3487 igvm_firmware: IgvmFirmwareSource::File(resolver.require(LATEST_STANDARD_X64).erase()),
3488 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
3489 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
3490 openhcl_config: OpenHclConfig {
3491 vmbus_redirect: true,
3493 ..Default::default()
3494 },
3495 }
3496 }
3497
3498 pub fn uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch, guest: UefiGuest) -> Self {
3500 use petri_artifacts_vmm_test::artifacts::loadable::*;
3501 let uefi_firmware = match arch {
3502 MachineArch::X86_64 => resolver.require(UEFI_FIRMWARE_X64).erase(),
3503 MachineArch::Aarch64 => resolver.require(UEFI_FIRMWARE_AARCH64).erase(),
3504 };
3505 Firmware::Uefi {
3506 guest,
3507 uefi_firmware,
3508 uefi_config: Default::default(),
3509 }
3510 }
3511
3512 pub fn openhcl_uefi(
3514 resolver: &ArtifactResolver<'_>,
3515 arch: MachineArch,
3516 guest: UefiGuest,
3517 isolation: Option<IsolationType>,
3518 ) -> Self {
3519 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
3520 let igvm_firmware = IgvmFirmwareSource::File(match arch {
3521 MachineArch::X86_64 if isolation.is_some() => resolver.require(LATEST_CVM_X64).erase(),
3522 MachineArch::X86_64 => resolver.require(LATEST_STANDARD_X64).erase(),
3523 MachineArch::Aarch64 => resolver.require(LATEST_STANDARD_AARCH64).erase(),
3524 });
3525 Firmware::OpenhclUefi {
3526 guest,
3527 isolation,
3528 igvm_firmware,
3529 uefi_config: Default::default(),
3530 openhcl_config: Default::default(),
3531 }
3532 }
3533
3534 fn is_openhcl(&self) -> bool {
3535 match self {
3536 Firmware::OpenhclLinuxDirect { .. }
3537 | Firmware::OpenhclUefi { .. }
3538 | Firmware::OpenhclPcat { .. } => true,
3539 Firmware::LinuxDirect { .. }
3540 | Firmware::IgvmLinuxDirect { .. }
3541 | Firmware::Pcat { .. }
3542 | Firmware::Uefi { .. } => false,
3543 }
3544 }
3545
3546 fn isolation(&self) -> Option<IsolationType> {
3547 match self {
3548 Firmware::IgvmLinuxDirect { isolation, .. } => Some(*isolation),
3549 Firmware::OpenhclUefi { isolation, .. } => *isolation,
3550 Firmware::LinuxDirect { .. }
3551 | Firmware::Pcat { .. }
3552 | Firmware::Uefi { .. }
3553 | Firmware::OpenhclLinuxDirect { .. }
3554 | Firmware::OpenhclPcat { .. } => None,
3555 }
3556 }
3557
3558 fn is_isolated_without_paravisor(&self) -> bool {
3560 self.isolation().is_some() && !self.is_openhcl()
3561 }
3562
3563 fn is_linux_direct(&self) -> bool {
3564 match self {
3565 Firmware::LinuxDirect { .. }
3566 | Firmware::IgvmLinuxDirect { .. }
3567 | Firmware::OpenhclLinuxDirect { .. } => true,
3568 Firmware::Pcat { .. }
3569 | Firmware::Uefi { .. }
3570 | Firmware::OpenhclUefi { .. }
3571 | Firmware::OpenhclPcat { .. } => false,
3572 }
3573 }
3574
3575 pub fn linux_direct_initrd(&self) -> Option<&Path> {
3577 match self {
3578 Firmware::LinuxDirect { initrd, .. } => Some(initrd.get()),
3579 _ => None,
3580 }
3581 }
3582
3583 fn is_pcat(&self) -> bool {
3584 match self {
3585 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => true,
3586 Firmware::Uefi { .. }
3587 | Firmware::OpenhclUefi { .. }
3588 | Firmware::LinuxDirect { .. }
3589 | Firmware::IgvmLinuxDirect { .. }
3590 | Firmware::OpenhclLinuxDirect { .. } => false,
3591 }
3592 }
3593
3594 fn os_flavor(&self) -> OsFlavor {
3595 match self {
3596 Firmware::LinuxDirect { .. }
3597 | Firmware::OpenhclLinuxDirect { .. }
3598 | Firmware::Uefi {
3599 guest: UefiGuest::Mshv(_),
3600 ..
3601 }
3602 | Firmware::OpenhclUefi {
3603 guest: UefiGuest::Mshv(_),
3604 ..
3605 }
3606 | Firmware::IgvmLinuxDirect { .. } => OsFlavor::Linux,
3607 Firmware::Uefi {
3608 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
3609 ..
3610 }
3611 | Firmware::OpenhclUefi {
3612 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
3613 ..
3614 } => OsFlavor::Uefi,
3615 Firmware::Pcat {
3616 guest: PcatGuest::Vhd(cfg),
3617 ..
3618 }
3619 | Firmware::OpenhclPcat {
3620 guest: PcatGuest::Vhd(cfg),
3621 ..
3622 }
3623 | Firmware::Uefi {
3624 guest: UefiGuest::Vhd(cfg),
3625 ..
3626 }
3627 | Firmware::OpenhclUefi {
3628 guest: UefiGuest::Vhd(cfg),
3629 ..
3630 } => cfg.os_flavor,
3631 Firmware::Pcat {
3632 guest: PcatGuest::Iso(cfg),
3633 ..
3634 }
3635 | Firmware::OpenhclPcat {
3636 guest: PcatGuest::Iso(cfg),
3637 ..
3638 } => cfg.os_flavor,
3639 }
3640 }
3641
3642 fn quirks(&self) -> GuestQuirks {
3643 match self {
3644 Firmware::Pcat {
3645 guest: PcatGuest::Vhd(cfg),
3646 ..
3647 }
3648 | Firmware::Uefi {
3649 guest: UefiGuest::Vhd(cfg),
3650 ..
3651 }
3652 | Firmware::OpenhclUefi {
3653 guest: UefiGuest::Vhd(cfg),
3654 ..
3655 } => cfg.quirks.clone(),
3656 Firmware::Pcat {
3657 guest: PcatGuest::Iso(cfg),
3658 ..
3659 } => cfg.quirks.clone(),
3660 _ => Default::default(),
3661 }
3662 }
3663
3664 fn expected_boot_event(&self) -> Option<FirmwareEvent> {
3665 match self {
3666 Firmware::LinuxDirect { .. }
3667 | Firmware::IgvmLinuxDirect { .. }
3668 | Firmware::OpenhclLinuxDirect { .. }
3669 | Firmware::Uefi {
3670 guest: UefiGuest::GuestTestUefi(_),
3671 ..
3672 }
3673 | Firmware::OpenhclUefi {
3674 guest: UefiGuest::GuestTestUefi(_),
3675 ..
3676 } => None,
3677 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => {
3678 Some(FirmwareEvent::BootAttempt)
3680 }
3681 Firmware::Uefi {
3682 guest: UefiGuest::None,
3683 ..
3684 }
3685 | Firmware::OpenhclUefi {
3686 guest: UefiGuest::None,
3687 ..
3688 } => Some(FirmwareEvent::NoBootDevice),
3689 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => {
3690 Some(FirmwareEvent::BootSuccess)
3691 }
3692 }
3693 }
3694
3695 fn openhcl_config(&self) -> Option<&OpenHclConfig> {
3696 match self {
3697 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
3698 | Firmware::OpenhclUefi { openhcl_config, .. }
3699 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
3700 Firmware::LinuxDirect { .. }
3701 | Firmware::IgvmLinuxDirect { .. }
3702 | Firmware::Pcat { .. }
3703 | Firmware::Uefi { .. } => None,
3704 }
3705 }
3706
3707 fn openhcl_config_mut(&mut self) -> Option<&mut OpenHclConfig> {
3708 match self {
3709 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
3710 | Firmware::OpenhclUefi { openhcl_config, .. }
3711 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
3712 Firmware::LinuxDirect { .. }
3713 | Firmware::IgvmLinuxDirect { .. }
3714 | Firmware::Pcat { .. }
3715 | Firmware::Uefi { .. } => None,
3716 }
3717 }
3718
3719 fn openhcl_firmware(&self) -> Option<&IgvmFirmwareSource> {
3720 match self {
3721 Firmware::OpenhclLinuxDirect { igvm_firmware, .. }
3722 | Firmware::OpenhclUefi { igvm_firmware, .. }
3723 | Firmware::OpenhclPcat { igvm_firmware, .. } => Some(igvm_firmware),
3724 Firmware::LinuxDirect { .. }
3725 | Firmware::IgvmLinuxDirect { .. }
3726 | Firmware::Pcat { .. }
3727 | Firmware::Uefi { .. } => None,
3728 }
3729 }
3730
3731 fn into_runtime_config(
3732 self,
3733 vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
3734 ) -> PetriVmRuntimeConfig {
3735 match self {
3736 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
3737 | Firmware::OpenhclUefi { openhcl_config, .. }
3738 | Firmware::OpenhclPcat { openhcl_config, .. } => PetriVmRuntimeConfig {
3739 vtl2_settings: Some(
3740 openhcl_config
3741 .vtl2_settings
3742 .unwrap_or_else(default_vtl2_settings),
3743 ),
3744 ide_controllers: None,
3745 vmbus_storage_controllers,
3746 },
3747 Firmware::Pcat {
3748 ide_controllers, ..
3749 } => PetriVmRuntimeConfig {
3750 vtl2_settings: None,
3751 ide_controllers: Some(ide_controllers),
3752 vmbus_storage_controllers,
3753 },
3754 Firmware::LinuxDirect { .. }
3755 | Firmware::IgvmLinuxDirect { .. }
3756 | Firmware::Uefi { .. } => PetriVmRuntimeConfig {
3757 vtl2_settings: None,
3758 ide_controllers: None,
3759 vmbus_storage_controllers,
3760 },
3761 }
3762 }
3763
3764 fn uefi_config(&self) -> Option<&UefiConfig> {
3765 match self {
3766 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
3767 Some(uefi_config)
3768 }
3769 Firmware::LinuxDirect { .. }
3770 | Firmware::IgvmLinuxDirect { .. }
3771 | Firmware::OpenhclLinuxDirect { .. }
3772 | Firmware::Pcat { .. }
3773 | Firmware::OpenhclPcat { .. } => None,
3774 }
3775 }
3776
3777 fn uefi_config_mut(&mut self) -> Option<&mut UefiConfig> {
3778 match self {
3779 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
3780 Some(uefi_config)
3781 }
3782 Firmware::LinuxDirect { .. }
3783 | Firmware::IgvmLinuxDirect { .. }
3784 | Firmware::OpenhclLinuxDirect { .. }
3785 | Firmware::Pcat { .. }
3786 | Firmware::OpenhclPcat { .. } => None,
3787 }
3788 }
3789
3790 fn boot_drive(&self, use_virtio_vsock: bool) -> anyhow::Result<Option<Drive>> {
3791 if let Firmware::Uefi {
3792 guest: UefiGuest::Mshv(boot),
3793 ..
3794 }
3795 | Firmware::OpenhclUefi {
3796 guest: UefiGuest::Mshv(boot),
3797 ..
3798 } = self
3799 {
3800 return Ok(Some(Drive::new(
3801 Some(
3802 boot.build(use_virtio_vsock)
3803 .context("composing mshv boot disk")?,
3804 ),
3805 false,
3806 )));
3807 }
3808 Ok(match self {
3809 Firmware::LinuxDirect { .. }
3810 | Firmware::IgvmLinuxDirect { .. }
3811 | Firmware::OpenhclLinuxDirect { .. } => None,
3812 Firmware::Pcat { guest, .. } | Firmware::OpenhclPcat { guest, .. } => {
3813 Some((guest.disk_path(), guest.is_dvd()))
3814 }
3815 Firmware::Uefi { guest, .. } | Firmware::OpenhclUefi { guest, .. } => {
3816 guest.disk_path().map(|dp| (dp, false))
3817 }
3818 }
3819 .map(|(disk_path, is_dvd)| Drive::new(Some(Disk::Differencing(disk_path)), is_dvd)))
3820 }
3821
3822 fn vtl2_settings(&mut self) -> Option<&mut Vtl2Settings> {
3823 self.openhcl_config_mut()
3824 .map(|c| c.vtl2_settings.get_or_insert_with(default_vtl2_settings))
3825 }
3826
3827 fn ide_controllers(&self) -> Option<&[[Option<Drive>; 2]; 2]> {
3828 match self {
3829 Firmware::Pcat {
3830 ide_controllers, ..
3831 } => Some(ide_controllers),
3832 _ => None,
3833 }
3834 }
3835
3836 fn ide_controllers_mut(&mut self) -> Option<&mut [[Option<Drive>; 2]; 2]> {
3837 match self {
3838 Firmware::Pcat {
3839 ide_controllers, ..
3840 } => Some(ide_controllers),
3841 _ => None,
3842 }
3843 }
3844}
3845
3846#[derive(Debug, Clone)]
3849pub enum PcatGuest {
3850 Vhd(BootImageConfig<boot_image_type::Vhd>),
3852 Iso(BootImageConfig<boot_image_type::Iso>),
3854}
3855
3856impl PcatGuest {
3857 fn disk_path(&self) -> DiskPath {
3858 match self {
3859 PcatGuest::Vhd(disk) => disk.disk_path(),
3860 PcatGuest::Iso(disk) => disk.disk_path(),
3861 }
3862 }
3863
3864 fn is_dvd(&self) -> bool {
3865 matches!(self, Self::Iso(_))
3866 }
3867}
3868
3869#[derive(Debug, Clone)]
3872pub enum UefiGuest {
3873 Mshv(MshvBoot),
3876 Vhd(BootImageConfig<boot_image_type::Vhd>),
3878 GuestTestUefi(ResolvedArtifact),
3880 None,
3882}
3883
3884impl UefiGuest {
3885 pub fn guest_test_uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
3887 use petri_artifacts_vmm_test::artifacts::test_vhd::*;
3888 let artifact = match arch {
3889 MachineArch::X86_64 => resolver.require(GUEST_TEST_UEFI_X64).erase(),
3890 MachineArch::Aarch64 => resolver.require(GUEST_TEST_UEFI_AARCH64).erase(),
3891 };
3892 UefiGuest::GuestTestUefi(artifact)
3893 }
3894
3895 fn disk_path(&self) -> Option<DiskPath> {
3896 match self {
3897 UefiGuest::Vhd(vhd) => Some(vhd.disk_path()),
3898 UefiGuest::GuestTestUefi(p) => Some(DiskPath::Local(p.get().to_path_buf())),
3899 UefiGuest::None => None,
3900 UefiGuest::Mshv(_) => None,
3901 }
3902 }
3903}
3904
3905pub mod boot_image_type {
3907 mod private {
3908 pub trait Sealed {}
3909 impl Sealed for super::Vhd {}
3910 impl Sealed for super::Iso {}
3911 }
3912
3913 pub trait BootImageType: private::Sealed {}
3916
3917 #[derive(Debug, Clone)]
3919 pub enum Vhd {}
3920
3921 #[derive(Debug, Clone)]
3923 pub enum Iso {}
3924
3925 impl BootImageType for Vhd {}
3926 impl BootImageType for Iso {}
3927}
3928
3929#[derive(Debug, Clone)]
3931pub struct BootImageConfig<T: boot_image_type::BootImageType> {
3932 artifact: ResolvedArtifactSource,
3934 os_flavor: OsFlavor,
3936 quirks: GuestQuirks,
3940 _type: core::marker::PhantomData<T>,
3942}
3943
3944impl<T: boot_image_type::BootImageType> BootImageConfig<T> {
3945 fn disk_path(&self) -> DiskPath {
3947 match self.artifact.get() {
3948 ArtifactSource::Local(p) => DiskPath::Local(p.clone()),
3949 ArtifactSource::Remote { url } => DiskPath::Remote { url: url.clone() },
3950 }
3951 }
3952}
3953
3954impl BootImageConfig<boot_image_type::Vhd> {
3955 pub fn from_vhd<A>(artifact: ResolvedArtifactSource<A>) -> Self
3957 where
3958 A: petri_artifacts_common::tags::IsTestVhd,
3959 {
3960 BootImageConfig {
3961 artifact: artifact.erase(),
3962 os_flavor: A::OS_FLAVOR,
3963 quirks: A::quirks(),
3964 _type: std::marker::PhantomData,
3965 }
3966 }
3967}
3968
3969impl BootImageConfig<boot_image_type::Iso> {
3970 pub fn from_iso<A>(artifact: ResolvedArtifactSource<A>) -> Self
3972 where
3973 A: petri_artifacts_common::tags::IsTestIso,
3974 {
3975 BootImageConfig {
3976 artifact: artifact.erase(),
3977 os_flavor: A::OS_FLAVOR,
3978 quirks: A::quirks(),
3979 _type: std::marker::PhantomData,
3980 }
3981 }
3982}
3983
3984#[derive(Debug, Clone, Copy)]
3986pub enum IsolationType {
3987 Vbs,
3989 Snp,
3991 Tdx,
3993}
3994
3995#[derive(Debug, Clone, Copy)]
3997pub struct OpenHclServicingFlags {
3998 pub enable_nvme_keepalive: bool,
4001 pub enable_mana_keepalive: bool,
4003 pub override_version_checks: bool,
4005 pub stop_timeout_hint_secs: Option<u16>,
4007}
4008
4009#[derive(Debug, Clone)]
4011pub enum DiskPath {
4012 Local(PathBuf),
4014 Remote {
4016 url: String,
4018 },
4019}
4020
4021impl From<PathBuf> for DiskPath {
4022 fn from(path: PathBuf) -> Self {
4023 DiskPath::Local(path)
4024 }
4025}
4026
4027#[derive(Debug, Clone)]
4029pub enum Disk {
4030 Memory(u64),
4032 Differencing(DiskPath),
4034 SqliteDifferencing {
4038 base: DiskPath,
4040 dbhd_path: PathBuf,
4042 },
4043 Persistent(PathBuf),
4045 Temporary(Arc<TempPath>),
4047}
4048
4049#[derive(Debug, Clone)]
4051pub enum TempOrPersistentPath {
4052 Temp(Arc<TempPath>),
4054 Persistent(PathBuf),
4056}
4057
4058impl AsRef<Path> for TempOrPersistentPath {
4059 fn as_ref(&self) -> &Path {
4060 match self {
4061 TempOrPersistentPath::Temp(temp_path) => temp_path.as_ref(),
4062 TempOrPersistentPath::Persistent(path_buf) => path_buf.as_ref(),
4063 }
4064 }
4065}
4066
4067#[derive(Debug, Clone)]
4069pub struct PetriInitrd {
4070 pub path: TempOrPersistentPath,
4072 pub rdinit_param: String,
4074}
4075
4076#[derive(Debug, Clone)]
4078pub struct PetriVmgsDisk {
4079 pub disk: Disk,
4081 pub encryption_policy: GuestStateEncryptionPolicy,
4083}
4084
4085impl Default for PetriVmgsDisk {
4086 fn default() -> Self {
4087 PetriVmgsDisk {
4088 disk: Disk::Memory(vmgs_format::VMGS_DEFAULT_CAPACITY),
4089 encryption_policy: GuestStateEncryptionPolicy::None(false),
4091 }
4092 }
4093}
4094
4095#[derive(Debug, Clone)]
4097pub enum PetriVmgsResource {
4098 Disk(PetriVmgsDisk),
4100 ReprovisionOnFailure(PetriVmgsDisk),
4102 ReprovisionSingle(PetriVmgsDisk),
4104 Ephemeral,
4106}
4107
4108impl PetriVmgsResource {
4109 pub fn vmgs(&self) -> Option<&PetriVmgsDisk> {
4111 match self {
4112 PetriVmgsResource::Disk(vmgs)
4113 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
4114 | PetriVmgsResource::ReprovisionSingle(vmgs) => Some(vmgs),
4115 PetriVmgsResource::Ephemeral => None,
4116 }
4117 }
4118
4119 pub fn disk(&self) -> Option<&Disk> {
4121 self.vmgs().map(|vmgs| &vmgs.disk)
4122 }
4123
4124 pub fn encryption_policy(&self) -> Option<GuestStateEncryptionPolicy> {
4126 self.vmgs().map(|vmgs| vmgs.encryption_policy)
4127 }
4128}
4129
4130#[derive(Debug, Clone, Copy)]
4132pub enum PetriGuestStateLifetime {
4133 Disk,
4136 ReprovisionOnFailure,
4138 ReprovisionSingle,
4140 Ephemeral,
4142}
4143
4144#[derive(Debug, Clone, Copy)]
4146pub enum SecureBootTemplate {
4147 MicrosoftWindows,
4149 MicrosoftUefiCertificateAuthority,
4151}
4152
4153#[derive(Default, Debug, Clone)]
4156pub struct VmmQuirks {
4157 pub flaky_boot: Option<Duration>,
4160}
4161
4162#[derive(Debug, Clone, Copy, Eq, PartialEq)]
4164pub enum PetriHaltReason {
4165 PowerOff,
4167 Reset,
4169 Hibernate,
4171 TripleFault,
4173 Other,
4175}
4176
4177impl PetriHaltReason {
4178 pub fn with_detail(self, detail: String) -> PetriHaltReasonDetail {
4180 PetriHaltReasonDetail {
4181 reason: self,
4182 detail,
4183 }
4184 }
4185}
4186
4187#[derive(Debug, Clone)]
4189pub struct PetriHaltReasonDetail {
4190 pub reason: PetriHaltReason,
4192 pub detail: String,
4194}
4195
4196pub(crate) fn linux_vsock_blacklist(use_virtio_vsock: bool) -> &'static str {
4197 if use_virtio_vsock {
4198 "initcall_blacklist=hv_sock_init"
4199 } else {
4200 "initcall_blacklist=virtio_vsock_init"
4201 }
4202}
4203
4204fn append_cmdline(cmd: &mut Option<String>, add_cmd: impl AsRef<str>) {
4205 if let Some(cmd) = cmd.as_mut() {
4206 cmd.push(' ');
4207 cmd.push_str(add_cmd.as_ref());
4208 } else {
4209 *cmd = Some(add_cmd.as_ref().to_string());
4210 }
4211}
4212
4213async fn collect_inspect(
4214 name: &'static str,
4215 inspect: impl Future<Output = anyhow::Result<inspect::Node>>,
4216 log_source: &PetriLogSource,
4217 prefix: &str,
4218) {
4219 let node = match CancelContext::new()
4220 .with_timeout(INSPECT_TIMEOUT)
4221 .until_cancelled(inspect)
4222 .await
4223 {
4224 Ok(Ok(node)) => node,
4225 Ok(Err(e)) => {
4226 tracing::warn!(name, ?e, "failed to collect inspect state");
4227 return;
4228 }
4229 Err(_) => {
4230 tracing::warn!(name, "timed out collecting inspect state");
4231 return;
4232 }
4233 };
4234
4235 if let Err(e) = log_source.write_attachment(
4236 &format!("{prefix}_inspect_{name}.log"),
4237 format!("{node:#}").as_bytes(),
4238 ) {
4239 tracing::error!(name, ?e, "failed to save inspect log");
4240 }
4241}
4242
4243pub struct ModifyFn<T>(pub Box<dyn FnOnce(T) -> T + Send>);
4245
4246impl<T> Debug for ModifyFn<T> {
4247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4248 write!(f, "_")
4249 }
4250}
4251
4252fn default_vtl2_settings() -> Vtl2Settings {
4254 Vtl2Settings {
4255 version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
4256 fixed: None,
4257 dynamic: Some(Default::default()),
4258 namespace_settings: Default::default(),
4259 }
4260}
4261
4262#[derive(Debug, Copy, Clone, PartialEq, Eq)]
4264pub enum Vtl {
4265 Vtl0 = 0,
4267 Vtl1 = 1,
4269 Vtl2 = 2,
4271}
4272
4273#[derive(Debug, Copy, Clone, PartialEq, Eq)]
4275pub enum VmbusStorageType {
4276 Scsi,
4278 Nvme,
4280 VirtioBlk,
4282}
4283
4284#[derive(Debug, Clone)]
4286pub struct Drive {
4287 pub disk: Option<Disk>,
4289 pub is_dvd: bool,
4291}
4292
4293impl Drive {
4294 pub fn new(disk: Option<Disk>, is_dvd: bool) -> Self {
4296 Self { disk, is_dvd }
4297 }
4298}
4299
4300#[derive(Debug, Clone)]
4302pub struct VmbusStorageController {
4303 pub target_vtl: Vtl,
4305 pub controller_type: VmbusStorageType,
4307 pub drives: HashMap<u32, Drive>,
4309}
4310
4311impl VmbusStorageController {
4312 pub fn new(target_vtl: Vtl, controller_type: VmbusStorageType) -> Self {
4314 Self {
4315 target_vtl,
4316 controller_type,
4317 drives: HashMap::new(),
4318 }
4319 }
4320
4321 pub fn set_drive(
4323 &mut self,
4324 lun: Option<u32>,
4325 drive: Drive,
4326 allow_modify_existing: bool,
4327 ) -> u32 {
4328 let lun = lun.unwrap_or_else(|| {
4329 let mut lun = None;
4331 for x in 0..u8::MAX as u32 {
4332 if !self.drives.contains_key(&x) {
4333 lun = Some(x);
4334 break;
4335 }
4336 }
4337 lun.expect("all locations on this controller are in use")
4338 });
4339
4340 if self.drives.insert(lun, drive).is_some() && !allow_modify_existing {
4341 panic!("a disk with lun {lun} already existed on this controller");
4342 }
4343
4344 lun
4345 }
4346}
4347
4348pub(crate) fn petri_disk_cache_dir() -> String {
4350 if let Ok(dir) = std::env::var("PETRI_CACHE_DIR") {
4351 return dir;
4352 }
4353
4354 #[cfg(target_os = "macos")]
4355 {
4356 if let Ok(home) = std::env::var("HOME") {
4357 return format!("{home}/Library/Caches/petri");
4358 }
4359 }
4360
4361 #[cfg(windows)]
4362 {
4363 if let Ok(local) = std::env::var("LOCALAPPDATA") {
4364 return format!("{local}\\petri\\cache");
4365 }
4366 }
4367
4368 if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
4370 return format!("{xdg}/petri");
4371 }
4372 if let Ok(home) = std::env::var("HOME") {
4373 return format!("{home}/.cache/petri");
4374 }
4375
4376 ".cache/petri".to_string()
4377}
4378
4379pub struct NestedTestDeps {
4381 pub archive: ErasedArtifactHandle,
4383 pub binary: String,
4385 pub test_name: String,
4387}
4388
4389impl NestedTestDeps {
4390 pub fn new<A: IsNextestArchive>(
4392 archive: ArtifactHandle<A>,
4393 binary: &str,
4394 test_name: &str,
4395 ) -> Self {
4396 Self {
4397 archive: archive.erase(),
4398 binary: binary.into(),
4399 test_name: test_name.into(),
4400 }
4401 }
4402}
4403
4404pub struct ResolvedNestedTestDeps {
4406 pub test_binary: TempPath,
4408 pub test_name: String,
4410 pub artifacts: Vec<(ErasedArtifactHandle, PathBuf)>,
4412}
4413
4414mod nextest_binaries_metadata_schema {
4415 use serde::Deserialize;
4416 use serde::Serialize;
4417 use std::collections::HashMap;
4418 use std::path::PathBuf;
4419
4420 #[derive(Serialize, Deserialize)]
4421 #[serde(rename_all = "kebab-case")]
4422 pub struct RustBuildMeta {
4423 pub target_directory: PathBuf,
4424 }
4425
4426 #[derive(Serialize, Deserialize)]
4427 #[serde(rename_all = "kebab-case")]
4428 pub struct Binary {
4429 pub binary_name: String,
4430 pub binary_path: PathBuf,
4431 }
4432
4433 #[derive(Serialize, Deserialize)]
4434 #[serde(rename_all = "kebab-case")]
4435 pub struct BinariesMetadata {
4436 pub rust_build_meta: RustBuildMeta,
4437 pub rust_binaries: HashMap<String, Binary>,
4438 }
4439}
4440
4441impl NestedTestDeps {
4442 fn extract_binary(&self) -> anyhow::Result<TempPath> {
4444 let temp_dir = tempfile::tempdir()?;
4445
4446 let archive = resolve_artifact(self.archive)?;
4449
4450 let mut cmd = std::process::Command::new("tar");
4451 cmd.arg("--zstd")
4452 .arg("-xvf")
4453 .arg(archive)
4454 .current_dir(&temp_dir);
4455 futures::executor::block_on(crate::run_host_cmd(cmd))?;
4456
4457 let meta_file = fs_err::File::open(
4458 temp_dir
4459 .path()
4460 .join("target")
4461 .join("nextest")
4462 .join("binaries-metadata.json"),
4463 )?;
4464 let meta: nextest_binaries_metadata_schema::BinariesMetadata =
4465 serde_json::from_reader(meta_file)?;
4466
4467 let build_binary_path = meta
4468 .rust_binaries
4469 .iter()
4470 .find(|(_, b)| b.binary_name == self.binary)
4471 .context("binary missing")?
4472 .1
4473 .binary_path
4474 .clone();
4475
4476 let local_binary_path = temp_dir.path().join(
4477 build_binary_path
4478 .strip_prefix(meta.rust_build_meta.target_directory.parent().unwrap())?,
4479 );
4480
4481 let test_content_temp_dir =
4483 PathBuf::from(std::env::var(VMM_TESTS_CONTENT_DIR)?).join(NESTED_TEMP_DIR);
4484 fs_err::create_dir_all(&test_content_temp_dir)?;
4485 let temp_path = tempfile::NamedTempFile::new_in(&test_content_temp_dir)
4486 .context("failed to create temp nested test")?
4487 .into_temp_path();
4488
4489 fs_err::copy(&local_binary_path, &temp_path)?;
4490
4491 Ok(temp_path)
4492 }
4493
4494 fn resolve(&self, discover_artifacts: bool) -> anyhow::Result<ResolvedNestedTestDeps> {
4495 let test_binary = self
4496 .extract_binary()
4497 .context("failed to extract nextest binary")?;
4498
4499 let artifacts = if discover_artifacts {
4500 petri_artifacts_core::query_test_binary_artifacts(&test_binary, &[&self.test_name])
4501 .context("failed to query test binary for artifacts")?
4502 .into_artifacts_list()
4503 .into_iter()
4504 .filter(|id| {
4505 id != petri_artifacts_common::artifacts::TEST_LOG_DIRECTORY::GLOBAL_UNIQUE_ID
4506 })
4507 .map(|id| {
4508 let handle = petri_artifacts_core::artifact_from_id(&id)
4509 .with_context(|| format!("unknown artifact handle: {id}"))?;
4510 Ok((handle, resolve_artifact(handle)?))
4511 })
4512 .collect::<anyhow::Result<Vec<_>>>()?
4513 } else {
4514 Vec::new()
4515 };
4516
4517 Ok(ResolvedNestedTestDeps {
4518 test_binary,
4519 test_name: self.test_name.clone(),
4520 artifacts,
4521 })
4522 }
4523}
4524
4525trait PetriGuestPaths {
4526 fn os_flavor(&self) -> OsFlavor;
4528
4529 fn path_separator(&self) -> &'static str {
4531 match self.os_flavor() {
4532 OsFlavor::Windows => "\\",
4533 _ => "/",
4534 }
4535 }
4536
4537 fn agent_disk_root(&self) -> &'static str {
4541 match self.os_flavor() {
4542 OsFlavor::Windows => "D:",
4543 _ => "/cidata",
4544 }
4545 }
4546
4547 fn build_guest_path(&self, components: &[String]) -> String {
4550 components.join(self.path_separator())
4551 }
4552
4553 fn convert_guest_path(&self, path: &Path) -> String {
4556 self.build_guest_path(
4557 &path
4558 .iter()
4559 .map(|c| c.to_str().expect("path not utf8").to_string())
4560 .collect::<Vec<_>>(),
4561 )
4562 }
4563
4564 fn parents<'a>(&self, path: &'a str) -> Vec<&'a str> {
4566 path.match_indices(self.path_separator())
4567 .map(|(x, _)| &path[0..x])
4568 .collect()
4569 }
4570
4571 fn agent_disk_path(&self, components: &[String]) -> String {
4575 self.build_guest_path(&[&[self.agent_disk_root().into()], components].concat())
4576 }
4577
4578 fn binary_with_extension(&self, name: &str) -> String {
4580 match self.os_flavor() {
4581 OsFlavor::Windows => format!("{name}.exe"),
4582 _ => name.into(),
4583 }
4584 }
4585}
4586
4587struct FatFsPathBuilder(OsFlavor);
4588
4589impl PetriGuestPaths for FatFsPathBuilder {
4590 fn os_flavor(&self) -> OsFlavor {
4591 self.0
4592 }
4593
4594 fn path_separator(&self) -> &'static str {
4595 "/"
4596 }
4597}
4598
4599impl<T: PetriVmmBackend> PetriGuestPaths for PetriVm<T> {
4600 fn os_flavor(&self) -> OsFlavor {
4601 self.properties.os_flavor
4602 }
4603}
4604
4605impl<T: PetriVmmBackend> PetriGuestPaths for PetriVmBuilder<T> {
4606 fn os_flavor(&self) -> OsFlavor {
4607 self.os_flavor()
4608 }
4609}
4610
4611#[cfg(test)]
4612mod tests {
4613 use super::*;
4614 use crate::Drive;
4615 use crate::VmbusStorageController;
4616 use crate::VmbusStorageType;
4617 use crate::Vtl;
4618
4619 #[derive(Clone, Copy)]
4620 enum TestInspectBehavior {
4621 Success,
4622 Error,
4623 Panic,
4624 }
4625
4626 #[derive(Clone)]
4627 struct TestInspector(TestInspectBehavior);
4628
4629 #[async_trait::async_trait]
4630 impl PetriVmInspector for TestInspector {
4631 async fn inspect(&self, _path: &str) -> anyhow::Result<inspect::Node> {
4632 match self.0 {
4633 TestInspectBehavior::Success => Ok(inspect::Node::Unevaluated),
4634 TestInspectBehavior::Error => anyhow::bail!("inspect failed"),
4635 TestInspectBehavior::Panic => panic!("inspect panicked"),
4636 }
4637 }
4638 }
4639
4640 struct TestFramebuffer;
4641
4642 #[async_trait::async_trait]
4643 impl PetriVmFramebufferAccess for TestFramebuffer {
4644 async fn screenshot(
4645 &mut self,
4646 _image: &mut Vec<u8>,
4647 ) -> anyhow::Result<Option<VmScreenshotMeta>> {
4648 unreachable!()
4649 }
4650 }
4651
4652 struct TestRuntime(TestInspectBehavior);
4653
4654 #[async_trait::async_trait]
4655 impl PetriVmRuntime for TestRuntime {
4656 type VmInspector = TestInspector;
4657 type VmFramebufferAccess = TestFramebuffer;
4658
4659 async fn teardown(self) -> anyhow::Result<()> {
4660 Ok(())
4661 }
4662
4663 async fn wait_for_halt(
4664 &mut self,
4665 _allow_reset: bool,
4666 ) -> anyhow::Result<PetriHaltReasonDetail> {
4667 unreachable!()
4668 }
4669
4670 async fn wait_for_agent(&mut self, _set_high_vtl: bool) -> anyhow::Result<PipetteClient> {
4671 unreachable!()
4672 }
4673
4674 fn openhcl_diag(&self) -> Option<OpenHclDiagHandler> {
4675 None
4676 }
4677
4678 async fn wait_for_boot_event(
4679 &mut self,
4680 _timeout: Option<Duration>,
4681 ) -> anyhow::Result<Option<FirmwareEvent>> {
4682 unreachable!()
4683 }
4684
4685 async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()> {
4686 unreachable!()
4687 }
4688
4689 async fn send_enlightened_shutdown(&mut self, _kind: ShutdownKind) -> anyhow::Result<()> {
4690 unreachable!()
4691 }
4692
4693 async fn restart_openhcl(
4694 &mut self,
4695 _new_openhcl: &ResolvedArtifact,
4696 _flags: OpenHclServicingFlags,
4697 ) -> anyhow::Result<()> {
4698 unreachable!()
4699 }
4700
4701 async fn save_openhcl(
4702 &mut self,
4703 _new_openhcl: &ResolvedArtifact,
4704 _flags: OpenHclServicingFlags,
4705 ) -> anyhow::Result<()> {
4706 unreachable!()
4707 }
4708
4709 async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
4710 unreachable!()
4711 }
4712
4713 async fn update_command_line(&mut self, _command_line: &str) -> anyhow::Result<()> {
4714 unreachable!()
4715 }
4716
4717 fn inspector(&self) -> Option<Self::VmInspector> {
4718 Some(TestInspector(self.0))
4719 }
4720
4721 async fn reset(&mut self) -> anyhow::Result<()> {
4722 unreachable!()
4723 }
4724
4725 async fn set_vtl2_settings(&mut self, _settings: &Vtl2Settings) -> anyhow::Result<()> {
4726 unreachable!()
4727 }
4728
4729 async fn set_vmbus_drive(
4730 &mut self,
4731 _disk: &Drive,
4732 _controller_id: &Guid,
4733 _controller_location: u32,
4734 ) -> anyhow::Result<()> {
4735 unreachable!()
4736 }
4737 }
4738
4739 fn test_runtime_guard(
4740 driver: DefaultDriver,
4741 log_source: &PetriLogSource,
4742 behavior: TestInspectBehavior,
4743 ) -> PetriVmRuntimeGuard<TestRuntime> {
4744 PetriVmRuntimeGuard {
4745 runtime: Some(TestRuntime(behavior)),
4746 driver,
4747 log_source: log_source.clone(),
4748 capture_inspect_on_drop: true,
4749 }
4750 }
4751
4752 fn run_failed_test(
4753 log_source: &PetriLogSource,
4754 behavior: TestInspectBehavior,
4755 ) -> anyhow::Result<()> {
4756 let pool = pal_async::DefaultPool::new();
4757 let driver = pool.driver();
4758 drop(test_runtime_guard(driver.clone(), log_source, behavior));
4759 drop(driver);
4760 pool.run();
4761 anyhow::bail!("original test failure")
4762 }
4763
4764 fn inspect_attachment_count(log_source: &PetriLogSource) -> usize {
4765 fs_err::read_dir(log_source.output_dir())
4766 .unwrap()
4767 .filter_map(Result::ok)
4768 .filter(|entry| {
4769 entry
4770 .file_name()
4771 .to_string_lossy()
4772 .starts_with("failure_inspect_vmm")
4773 })
4774 .count()
4775 }
4776
4777 #[test]
4778 fn test_runtime_guard_failure_capture() {
4779 let temp_dir = tempfile::tempdir().unwrap();
4780 let log_source = crate::tracing::try_init_tracing(
4781 temp_dir.path(),
4782 tracing::level_filters::LevelFilter::DEBUG,
4783 None,
4784 )
4785 .unwrap();
4786
4787 let error = run_failed_test(&log_source, TestInspectBehavior::Success).unwrap_err();
4788 assert_eq!(error.to_string(), "original test failure");
4789 assert_eq!(inspect_attachment_count(&log_source), 1);
4790
4791 let pool = pal_async::DefaultPool::new();
4792 let driver = pool.driver();
4793 let mut guard =
4794 test_runtime_guard(driver.clone(), &log_source, TestInspectBehavior::Success);
4795 let _runtime = guard.take_for_teardown();
4796 drop(guard);
4797 drop(driver);
4798 pool.run();
4799 assert_eq!(inspect_attachment_count(&log_source), 1);
4800
4801 for behavior in [TestInspectBehavior::Error, TestInspectBehavior::Panic] {
4802 let error = run_failed_test(&log_source, behavior).unwrap_err();
4803 assert_eq!(error.to_string(), "original test failure");
4804 assert_eq!(inspect_attachment_count(&log_source), 1);
4805 }
4806 }
4807
4808 #[test]
4809 fn test_vmbus_storage_controller() {
4810 let mut controller = VmbusStorageController::new(Vtl::Vtl0, VmbusStorageType::Scsi);
4811 assert_eq!(
4812 controller.set_drive(Some(1), Drive::new(None, false), false),
4813 1
4814 );
4815 assert!(controller.drives.contains_key(&1));
4816 assert_eq!(
4817 controller.set_drive(None, Drive::new(None, false), false),
4818 0
4819 );
4820 assert!(controller.drives.contains_key(&0));
4821 assert_eq!(
4822 controller.set_drive(None, Drive::new(None, false), false),
4823 2
4824 );
4825 assert!(controller.drives.contains_key(&2));
4826 assert_eq!(
4827 controller.set_drive(Some(0), Drive::new(None, false), true),
4828 0
4829 );
4830 assert!(controller.drives.contains_key(&0));
4831 }
4832}