petri/vm/
mod.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4/// Hyper-V VM management
5#[cfg(windows)]
6pub mod hyperv;
7/// OpenVMM VM management
8pub mod openvmm;
9
10use crate::ShutdownKind;
11use async_trait::async_trait;
12use get_resources::ged::FirmwareEvent;
13use petri_artifacts_common::tags::GuestQuirks;
14use petri_artifacts_common::tags::IsTestVmgs;
15use petri_artifacts_common::tags::MachineArch;
16use petri_artifacts_common::tags::OsFlavor;
17use petri_artifacts_core::ArtifactResolver;
18use petri_artifacts_core::ResolvedArtifact;
19use petri_artifacts_core::ResolvedOptionalArtifact;
20use pipette_client::PipetteClient;
21use vmm_core_defs::HaltReason;
22
23/// Configuration state for a test VM.
24///
25/// R is the type of the struct used to interact with the VM once it is created
26#[async_trait]
27pub trait PetriVmConfig: Send {
28    /// Build and boot the requested VM. Does not configure and start pipette.
29    /// Should only be used for testing platforms that pipette does not support.
30    async fn run_without_agent(self: Box<Self>) -> anyhow::Result<Box<dyn PetriVm>>;
31    /// Run the VM, configuring pipette to automatically start, but do not wait
32    /// for it to connect. This is useful for tests where the first boot attempt
33    /// is expected to not succeed, but pipette functionality is still desired.
34    async fn run_with_lazy_pipette(self: Box<Self>) -> anyhow::Result<Box<dyn PetriVm>>;
35    /// Run the VM, launching pipette and returning a client to it.
36    async fn run(self: Box<Self>) -> anyhow::Result<(Box<dyn PetriVm>, PipetteClient)>;
37
38    /// Set the VM to enable secure boot and inject the templates per OS flavor.
39    fn with_secure_boot(self: Box<Self>) -> Box<dyn PetriVmConfig>;
40    /// Inject Windows secure boot templates into the VM's UEFI.
41    fn with_windows_secure_boot_template(self: Box<Self>) -> Box<dyn PetriVmConfig>;
42    /// Inject UEFI CA secure boot templates into the VM's UEFI.
43    fn with_uefi_ca_secure_boot_template(self: Box<Self>) -> Box<dyn PetriVmConfig>;
44    /// Set the VM to use the specified processor topology.
45    fn with_processor_topology(
46        self: Box<Self>,
47        topology: ProcessorTopology,
48    ) -> Box<dyn PetriVmConfig>;
49
50    /// Sets a custom OpenHCL IGVM file to use.
51    fn with_custom_openhcl(self: Box<Self>, artifact: ResolvedArtifact) -> Box<dyn PetriVmConfig>;
52    /// Sets the command line for the paravisor.
53    fn with_openhcl_command_line(self: Box<Self>, command_line: &str) -> Box<dyn PetriVmConfig>;
54    /// Adds a file to the VM's pipette agent image.
55    fn with_agent_file(
56        self: Box<Self>,
57        name: &str,
58        artifact: ResolvedArtifact,
59    ) -> Box<dyn PetriVmConfig>;
60    /// Adds a file to the paravisor's pipette agent image.
61    fn with_openhcl_agent_file(
62        self: Box<Self>,
63        name: &str,
64        artifact: ResolvedArtifact,
65    ) -> Box<dyn PetriVmConfig>;
66    /// Sets whether UEFI frontpage is enabled.
67    fn with_uefi_frontpage(self: Box<Self>, enable: bool) -> Box<dyn PetriVmConfig>;
68    /// Run the VM with Enable VMBus relay enabled
69    fn with_vmbus_redirect(self: Box<Self>, enable: bool) -> Box<dyn PetriVmConfig>;
70
71    /// Get the OS that the VM will boot into.
72    fn os_flavor(&self) -> OsFlavor;
73}
74
75/// Common processor topology information for the VM.
76pub struct ProcessorTopology {
77    /// The number of virtual processors.
78    pub vp_count: u32,
79    /// Whether SMT (hyperthreading) is enabled.
80    pub enable_smt: Option<bool>,
81    /// The number of virtual processors per socket.
82    pub vps_per_socket: Option<u32>,
83    /// The APIC configuration (x86-64 only).
84    pub apic_mode: Option<ApicMode>,
85}
86
87impl Default for ProcessorTopology {
88    fn default() -> Self {
89        Self {
90            vp_count: 2,
91            enable_smt: None,
92            vps_per_socket: None,
93            apic_mode: None,
94        }
95    }
96}
97
98/// The APIC mode for the VM.
99#[derive(Debug, Clone, Copy)]
100pub enum ApicMode {
101    /// xAPIC mode only.
102    Xapic,
103    /// x2APIC mode supported but not enabled at boot.
104    X2apicSupported,
105    /// x2APIC mode enabled at boot.
106    X2apicEnabled,
107}
108
109/// A running VM that tests can interact with.
110#[async_trait]
111pub trait PetriVm: Send {
112    /// Returns the guest architecture.
113    fn arch(&self) -> MachineArch;
114    /// Wait for the VM to halt, returning the reason for the halt.
115    async fn wait_for_halt(&mut self) -> anyhow::Result<HaltReason>;
116    /// Wait for the VM to halt, returning the reason for the halt,
117    /// and cleanly tear down the VM.
118    async fn wait_for_teardown(self: Box<Self>) -> anyhow::Result<HaltReason>;
119    /// Test that we are able to inspect OpenHCL.
120    async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()>;
121    /// Wait for a connection from a pipette agent running in the guest.
122    /// Useful if you've rebooted the vm or are otherwise expecting a fresh connection.
123    async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient>;
124    /// Wait for a connection from a pipette agent running in VTL 2.
125    /// Useful if you've reset VTL 2 or are otherwise expecting a fresh connection.
126    /// Will fail if the VM is not running OpenHCL.
127    async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient>;
128    /// Wait for VTL 2 to report that it is ready to respond to commands.
129    /// Will fail if the VM is not running OpenHCL.
130    ///
131    /// This should only be necessary if you're doing something manual. All
132    /// Petri-provided methods will wait for VTL 2 to be ready automatically.
133    async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()>;
134    /// Waits for an event emitted by the firmware about its boot status, and
135    /// verifies that it is the expected success value.
136    ///
137    /// * Linux Direct guests do not emit a boot event, so this method immediately returns Ok.
138    /// * PCAT guests may not emit an event depending on the PCAT version, this
139    /// method is best effort for them.
140    async fn wait_for_successful_boot_event(&mut self) -> anyhow::Result<()>;
141    /// Waits for an event emitted by the firmware about its boot status, and
142    /// returns that status.
143    async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent>;
144    /// Instruct the guest to shutdown via the Hyper-V shutdown IC.
145    async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
146}
147
148/// Firmware to load into the test VM.
149#[derive(Debug)]
150pub enum Firmware {
151    /// Boot Linux directly, without any firmware.
152    LinuxDirect {
153        /// The kernel to boot.
154        kernel: ResolvedArtifact,
155        /// The initrd to use.
156        initrd: ResolvedArtifact,
157    },
158    /// Boot Linux directly, without any firmware, with OpenHCL in VTL2.
159    OpenhclLinuxDirect {
160        /// The path to the IGVM file to use.
161        igvm_path: ResolvedArtifact,
162    },
163    /// Boot a PCAT-based VM.
164    Pcat {
165        /// The guest OS the VM will boot into.
166        guest: PcatGuest,
167        /// The firmware to use.
168        bios_firmware: ResolvedOptionalArtifact,
169        /// The SVGA firmware to use.
170        svga_firmware: ResolvedOptionalArtifact,
171    },
172    /// Boot a UEFI-based VM.
173    Uefi {
174        /// The guest OS the VM will boot into.
175        guest: UefiGuest,
176        /// The firmware to use.
177        uefi_firmware: ResolvedArtifact,
178    },
179    /// Boot a UEFI-based VM with OpenHCL in VTL2.
180    OpenhclUefi {
181        /// The guest OS the VM will boot into.
182        guest: UefiGuest,
183        /// The isolation type of the VM.
184        isolation: Option<IsolationType>,
185        /// Emulate SCSI via NVME to VTL2, with the provided namespace ID on
186        /// the controller with `BOOT_NVME_INSTANCE`.
187        vtl2_nvme_boot: bool,
188        /// The path to the IGVM file to use.
189        igvm_path: ResolvedArtifact,
190    },
191}
192
193impl Firmware {
194    /// Constructs a standard [`Firmware::LinuxDirect`] configuration.
195    pub fn linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
196        use petri_artifacts_vmm_test::artifacts::loadable::*;
197        match arch {
198            MachineArch::X86_64 => Firmware::LinuxDirect {
199                kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_X64).erase(),
200                initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
201            },
202            MachineArch::Aarch64 => Firmware::LinuxDirect {
203                kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_AARCH64).erase(),
204                initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_AARCH64).erase(),
205            },
206        }
207    }
208
209    /// Constructs a standard [`Firmware::OpenhclLinuxDirect`] configuration.
210    pub fn openhcl_linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
211        use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
212        match arch {
213            MachineArch::X86_64 => Firmware::OpenhclLinuxDirect {
214                igvm_path: resolver.require(LATEST_LINUX_DIRECT_TEST_X64).erase(),
215            },
216            MachineArch::Aarch64 => todo!("Linux direct not yet supported on aarch64"),
217        }
218    }
219
220    /// Constructs a standard [`Firmware::Pcat`] configuration.
221    pub fn pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
222        use petri_artifacts_vmm_test::artifacts::loadable::*;
223        Firmware::Pcat {
224            guest,
225            bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
226            svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
227        }
228    }
229
230    /// Constructs a standard [`Firmware::Uefi`] configuration.
231    pub fn uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch, guest: UefiGuest) -> Self {
232        use petri_artifacts_vmm_test::artifacts::loadable::*;
233        let uefi_firmware = match arch {
234            MachineArch::X86_64 => resolver.require(UEFI_FIRMWARE_X64).erase(),
235            MachineArch::Aarch64 => resolver.require(UEFI_FIRMWARE_AARCH64).erase(),
236        };
237        Firmware::Uefi {
238            guest,
239            uefi_firmware,
240        }
241    }
242
243    /// Constructs a standard [`Firmware::OpenhclUefi`] configuration.
244    pub fn openhcl_uefi(
245        resolver: &ArtifactResolver<'_>,
246        arch: MachineArch,
247        guest: UefiGuest,
248        isolation: Option<IsolationType>,
249        vtl2_nvme_boot: bool,
250    ) -> Self {
251        use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
252        let igvm_path = match arch {
253            MachineArch::X86_64 if isolation.is_some() => resolver.require(LATEST_CVM_X64).erase(),
254            MachineArch::X86_64 => resolver.require(LATEST_STANDARD_X64).erase(),
255            MachineArch::Aarch64 => resolver.require(LATEST_STANDARD_AARCH64).erase(),
256        };
257        Firmware::OpenhclUefi {
258            guest,
259            isolation,
260            vtl2_nvme_boot,
261            igvm_path,
262        }
263    }
264
265    fn is_openhcl(&self) -> bool {
266        match self {
267            Firmware::OpenhclLinuxDirect { .. } | Firmware::OpenhclUefi { .. } => true,
268            Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => false,
269        }
270    }
271
272    fn isolation(&self) -> Option<IsolationType> {
273        match self {
274            Firmware::OpenhclUefi { isolation, .. } => *isolation,
275            Firmware::LinuxDirect { .. }
276            | Firmware::Pcat { .. }
277            | Firmware::Uefi { .. }
278            | Firmware::OpenhclLinuxDirect { .. } => None,
279        }
280    }
281
282    fn is_linux_direct(&self) -> bool {
283        match self {
284            Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => true,
285            Firmware::Pcat { .. } | Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => false,
286        }
287    }
288
289    fn is_uefi(&self) -> bool {
290        match self {
291            Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => true,
292            Firmware::LinuxDirect { .. }
293            | Firmware::OpenhclLinuxDirect { .. }
294            | Firmware::Pcat { .. } => false,
295        }
296    }
297
298    fn os_flavor(&self) -> OsFlavor {
299        match self {
300            Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => OsFlavor::Linux,
301            Firmware::Uefi {
302                guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
303                ..
304            }
305            | Firmware::OpenhclUefi {
306                guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
307                ..
308            } => OsFlavor::Uefi,
309            Firmware::Pcat {
310                guest: PcatGuest::Vhd(cfg),
311                ..
312            }
313            | Firmware::Uefi {
314                guest: UefiGuest::Vhd(cfg),
315                ..
316            }
317            | Firmware::OpenhclUefi {
318                guest: UefiGuest::Vhd(cfg),
319                ..
320            } => cfg.os_flavor,
321            Firmware::Pcat {
322                guest: PcatGuest::Iso(cfg),
323                ..
324            } => cfg.os_flavor,
325        }
326    }
327
328    fn quirks(&self) -> GuestQuirks {
329        match self {
330            Firmware::Pcat {
331                guest: PcatGuest::Vhd(cfg),
332                ..
333            }
334            | Firmware::Uefi {
335                guest: UefiGuest::Vhd(cfg),
336                ..
337            }
338            | Firmware::OpenhclUefi {
339                guest: UefiGuest::Vhd(cfg),
340                ..
341            } => cfg.quirks,
342            Firmware::Pcat {
343                guest: PcatGuest::Iso(cfg),
344                ..
345            } => cfg.quirks,
346            _ => Default::default(),
347        }
348    }
349
350    fn expected_boot_event(&self) -> Option<FirmwareEvent> {
351        match self {
352            Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => None,
353            Firmware::Pcat { .. } => {
354                // TODO: Handle older PCAT versions that don't fire the event
355                Some(FirmwareEvent::BootAttempt)
356            }
357            Firmware::Uefi {
358                guest: UefiGuest::None,
359                ..
360            }
361            | Firmware::OpenhclUefi {
362                guest: UefiGuest::None,
363                ..
364            } => Some(FirmwareEvent::NoBootDevice),
365            Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => {
366                Some(FirmwareEvent::BootSuccess)
367            }
368        }
369    }
370}
371
372/// The guest the VM will boot into. A boot drive with the chosen setup
373/// will be automatically configured.
374#[derive(Debug)]
375pub enum PcatGuest {
376    /// Mount a VHD as the boot drive.
377    Vhd(BootImageConfig<boot_image_type::Vhd>),
378    /// Mount an ISO as the CD/DVD drive.
379    Iso(BootImageConfig<boot_image_type::Iso>),
380}
381
382impl PcatGuest {
383    fn artifact(&self) -> &ResolvedArtifact {
384        match self {
385            PcatGuest::Vhd(disk) => &disk.artifact,
386            PcatGuest::Iso(disk) => &disk.artifact,
387        }
388    }
389}
390
391/// The guest the VM will boot into. A boot drive with the chosen setup
392/// will be automatically configured.
393#[derive(Debug)]
394pub enum UefiGuest {
395    /// Mount a VHD as the boot drive.
396    Vhd(BootImageConfig<boot_image_type::Vhd>),
397    /// The UEFI test image produced by our guest-test infrastructure.
398    GuestTestUefi(ResolvedArtifact),
399    /// No guest, just the firmware.
400    None,
401}
402
403impl UefiGuest {
404    /// Construct a standard [`UefiGuest::GuestTestUefi`] configuration.
405    pub fn guest_test_uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
406        use petri_artifacts_vmm_test::artifacts::test_vhd::*;
407        let artifact = match arch {
408            MachineArch::X86_64 => resolver.require(GUEST_TEST_UEFI_X64).erase(),
409            MachineArch::Aarch64 => resolver.require(GUEST_TEST_UEFI_AARCH64).erase(),
410        };
411        UefiGuest::GuestTestUefi(artifact)
412    }
413
414    fn artifact(&self) -> Option<&ResolvedArtifact> {
415        match self {
416            UefiGuest::Vhd(vhd) => Some(&vhd.artifact),
417            UefiGuest::GuestTestUefi(p) => Some(p),
418            UefiGuest::None => None,
419        }
420    }
421}
422
423/// Type-tags for [`BootImageConfig`](super::BootImageConfig)
424pub mod boot_image_type {
425    mod private {
426        pub trait Sealed {}
427        impl Sealed for super::Vhd {}
428        impl Sealed for super::Iso {}
429    }
430
431    /// Private trait use to seal the set of artifact types BootImageType
432    /// supports.
433    pub trait BootImageType: private::Sealed {}
434
435    /// BootImageConfig for a VHD file
436    #[derive(Debug)]
437    pub enum Vhd {}
438
439    /// BootImageConfig for an ISO file
440    #[derive(Debug)]
441    pub enum Iso {}
442
443    impl BootImageType for Vhd {}
444    impl BootImageType for Iso {}
445}
446
447/// Configuration information for the boot drive of the VM.
448#[derive(Debug)]
449pub struct BootImageConfig<T: boot_image_type::BootImageType> {
450    /// Artifact handle corresponding to the boot media.
451    artifact: ResolvedArtifact,
452    /// The OS flavor.
453    os_flavor: OsFlavor,
454    /// Any quirks needed to boot the guest.
455    ///
456    /// Most guests should not need any quirks, and can use `Default`.
457    quirks: GuestQuirks,
458    /// Marker denoting what type of media `artifact` corresponds to
459    _type: core::marker::PhantomData<T>,
460}
461
462impl BootImageConfig<boot_image_type::Vhd> {
463    /// Create a new BootImageConfig from a VHD artifact handle
464    pub fn from_vhd<A>(artifact: ResolvedArtifact<A>) -> Self
465    where
466        A: petri_artifacts_common::tags::IsTestVhd,
467    {
468        BootImageConfig {
469            artifact: artifact.erase(),
470            os_flavor: A::OS_FLAVOR,
471            quirks: A::quirks(),
472            _type: std::marker::PhantomData,
473        }
474    }
475}
476
477impl BootImageConfig<boot_image_type::Iso> {
478    /// Create a new BootImageConfig from an ISO artifact handle
479    pub fn from_iso<A>(artifact: ResolvedArtifact<A>) -> Self
480    where
481        A: petri_artifacts_common::tags::IsTestIso,
482    {
483        BootImageConfig {
484            artifact: artifact.erase(),
485            os_flavor: A::OS_FLAVOR,
486            quirks: A::quirks(),
487            _type: std::marker::PhantomData,
488        }
489    }
490}
491
492/// Isolation type
493#[derive(Debug, Clone, Copy)]
494pub enum IsolationType {
495    /// VBS
496    Vbs,
497    /// SNP
498    Snp,
499    /// TDX
500    Tdx,
501}
502
503/// Flags controlling servicing behavior.
504#[derive(Default, Debug, Clone, Copy)]
505pub struct OpenHclServicingFlags {
506    /// Preserve DMA memory for NVMe devices if supported.
507    pub enable_nvme_keepalive: bool,
508}
509
510/// Virtual machine guest state resource
511pub enum PetriVmgsResource<T: IsTestVmgs> {
512    /// Use disk to store guest state
513    Disk(ResolvedArtifact<T>),
514    /// Use disk to store guest state, reformatting if corrupted.
515    ReprovisionOnFailure(ResolvedArtifact<T>),
516    /// Format and use disk to store guest state
517    Reprovision(ResolvedArtifact<T>),
518    /// Store guest state in memory
519    Ephemeral,
520}