petri/vm/openvmm/
modify.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Helpers to modify a [`PetriVmConfigOpenVmm`] from its defaults.
5
6use super::MANA_INSTANCE;
7use super::NIC_MAC_ADDRESS;
8use super::PetriVmConfigOpenVmm;
9use super::memdiff_disk_from_artifact;
10use crate::PetriVmgsResource;
11use crate::ProcessorTopology;
12use chipset_resources::battery::BatteryDeviceHandleX64;
13use chipset_resources::battery::HostBatteryUpdate;
14use fs_err::File;
15use gdma_resources::GdmaDeviceHandle;
16use gdma_resources::VportDefinition;
17use get_resources::ged::IgvmAttestTestConfig;
18use hvlite_defs::config::Config;
19use hvlite_defs::config::DeviceVtl;
20use hvlite_defs::config::LoadMode;
21use hvlite_defs::config::VpciDeviceConfig;
22use hvlite_defs::config::Vtl2BaseAddressType;
23use petri_artifacts_common::tags::IsTestVmgs;
24use petri_artifacts_common::tags::MachineArch;
25use petri_artifacts_common::tags::OsFlavor;
26use petri_artifacts_core::ResolvedArtifact;
27use tpm_resources::TpmDeviceHandle;
28use tpm_resources::TpmRegisterLayout;
29use vm_resource::IntoResource;
30use vmcore::non_volatile_store::resources::EphemeralNonVolatileStoreHandle;
31use vmgs_resources::VmgsResource;
32use vmotherboard::ChipsetDeviceHandle;
33use vtl2_settings_proto::Vtl2Settings;
34
35impl PetriVmConfigOpenVmm {
36    /// Enable VMBus redirection.
37    pub fn with_vmbus_redirect(mut self) -> Self {
38        self.config
39            .vmbus
40            .as_mut()
41            .expect("vmbus not configured")
42            .vtl2_redirect = true;
43
44        let Some(ged) = &mut self.ged else {
45            panic!("VMBus redirection is only supported for OpenHCL.")
46        };
47        ged.vmbus_redirection = true;
48
49        self
50    }
51
52    /// Enable the VTL0 alias map.
53    // TODO: Remove once #912 is fixed.
54    pub fn with_vtl0_alias_map(mut self) -> Self {
55        self.config
56            .hypervisor
57            .with_vtl2
58            .as_mut()
59            .expect("Not an openhcl config.")
60            .vtl0_alias_map = true;
61        self
62    }
63
64    /// Enable the TPM with ephemeral storage.
65    pub fn with_tpm(mut self) -> Self {
66        if self.firmware.is_openhcl() {
67            self.ged.as_mut().unwrap().enable_tpm = true;
68        } else {
69            self.config.chipset_devices.push(ChipsetDeviceHandle {
70                name: "tpm".to_string(),
71                resource: TpmDeviceHandle {
72                    ppi_store: EphemeralNonVolatileStoreHandle.into_resource(),
73                    nvram_store: EphemeralNonVolatileStoreHandle.into_resource(),
74                    refresh_tpm_seeds: false,
75                    ak_cert_type: tpm_resources::TpmAkCertTypeResource::None,
76                    register_layout: TpmRegisterLayout::IoPort,
77                    guest_secret_key: None,
78                    logger: None,
79                }
80                .into_resource(),
81            });
82            if let LoadMode::Uefi { enable_tpm, .. } = &mut self.config.load_mode {
83                *enable_tpm = true;
84            }
85        }
86
87        self
88    }
89
90    /// Set the VM to use the specified number of virtual processors.
91    ///
92    /// Using 1 CPU is useful for heavier OpenHCL tests, as our WHP emulation
93    /// layer is rather slow when dealing with cross-cpu communication.
94    pub fn with_processor_topology(mut self, topology: ProcessorTopology) -> Self {
95        let ProcessorTopology {
96            vp_count,
97            enable_smt,
98            vps_per_socket,
99            apic_mode,
100        } = topology;
101        self.config.processor_topology.proc_count = vp_count;
102        self.config.processor_topology.enable_smt = enable_smt;
103        self.config.processor_topology.vps_per_socket = vps_per_socket;
104        self.config.processor_topology.arch = Some(match self.arch {
105            MachineArch::X86_64 => hvlite_defs::config::ArchTopologyConfig::X86(
106                hvlite_defs::config::X86TopologyConfig {
107                    x2apic: match apic_mode {
108                        None => hvlite_defs::config::X2ApicConfig::Auto,
109                        Some(x) => match x {
110                            crate::ApicMode::Xapic => {
111                                hvlite_defs::config::X2ApicConfig::Unsupported
112                            }
113                            crate::ApicMode::X2apicSupported => {
114                                hvlite_defs::config::X2ApicConfig::Supported
115                            }
116                            crate::ApicMode::X2apicEnabled => {
117                                hvlite_defs::config::X2ApicConfig::Enabled
118                            }
119                        },
120                    },
121                    ..Default::default()
122                },
123            ),
124            MachineArch::Aarch64 => hvlite_defs::config::ArchTopologyConfig::Aarch64(
125                hvlite_defs::config::Aarch64TopologyConfig::default(),
126            ),
127        });
128        self
129    }
130
131    /// Set the VM to enable secure boot and inject the templates per OS flavor.
132    pub fn with_secure_boot(mut self) -> Self {
133        if !self.firmware.is_uefi() {
134            panic!("Secure boot is only supported for UEFI firmware.");
135        }
136
137        if self.firmware.is_openhcl() {
138            self.ged.as_mut().unwrap().secure_boot_enabled = true;
139        } else {
140            self.config.secure_boot_enabled = true;
141        }
142
143        match self.os_flavor() {
144            OsFlavor::Windows => self.with_windows_secure_boot_template(),
145            OsFlavor::Linux => self.with_uefi_ca_secure_boot_template(),
146            _ => panic!(
147                "Secure boot unsupported for OS flavor {:?}",
148                self.os_flavor()
149            ),
150        }
151    }
152
153    /// Inject Windows secure boot templates into the VM's UEFI.
154    pub fn with_windows_secure_boot_template(mut self) -> Self {
155        if !self.firmware.is_uefi() {
156            panic!("Secure boot is only supported for UEFI firmware.");
157        }
158
159        if self.firmware.is_openhcl() {
160            self.ged.as_mut().unwrap().secure_boot_template =
161                get_resources::ged::GuestSecureBootTemplateType::MicrosoftWindows;
162        } else {
163            self.config.custom_uefi_vars = hyperv_secure_boot_templates::x64::microsoft_windows();
164        }
165
166        self
167    }
168
169    /// Inject UEFI CA secure boot templates into the VM's UEFI.
170    pub fn with_uefi_ca_secure_boot_template(mut self) -> Self {
171        if !self.firmware.is_uefi() {
172            panic!("Secure boot is only supported for UEFI firmware.");
173        }
174
175        if self.firmware.is_openhcl() {
176            self.ged.as_mut().unwrap().secure_boot_template =
177                get_resources::ged::GuestSecureBootTemplateType::MicrosoftUefiCertificateAuthoritiy;
178        } else {
179            self.config.custom_uefi_vars = hyperv_secure_boot_templates::x64::microsoft_uefi_ca();
180        }
181
182        self
183    }
184
185    /// Enable the battery for the VM.
186    pub fn with_battery(mut self) -> Self {
187        if self.firmware.is_openhcl() {
188            self.ged.as_mut().unwrap().enable_battery = true;
189        } else {
190            self.config.chipset_devices.push(ChipsetDeviceHandle {
191                name: "battery".to_string(),
192                resource: BatteryDeviceHandleX64 {
193                    battery_status_recv: {
194                        let (tx, rx) = mesh::channel();
195                        tx.send(HostBatteryUpdate::default_present());
196                        rx
197                    },
198                }
199                .into_resource(),
200            });
201            if let LoadMode::Uefi { enable_battery, .. } = &mut self.config.load_mode {
202                *enable_battery = true;
203            }
204        }
205        self
206    }
207
208    /// Enable TPM state persistence
209    pub fn with_tpm_state_persistence(mut self) -> Self {
210        if !self.firmware.is_openhcl() {
211            panic!("TPM state persistence is only supported for OpenHCL.")
212        };
213
214        let ged = self.ged.as_mut().expect("No GED to configure TPM");
215
216        // Disable no_persistent_secrets implies preserving TPM states
217        // across boots
218        ged.no_persistent_secrets = false;
219
220        self
221    }
222
223    /// Set test config for the GED's IGVM attest request handler
224    pub fn with_igvm_attest_test_config(mut self, config: IgvmAttestTestConfig) -> Self {
225        if !self.firmware.is_openhcl() {
226            panic!("IGVM Attest test config is only supported for OpenHCL.")
227        };
228
229        let ged = self.ged.as_mut().expect("No GED to configure TPM");
230
231        ged.igvm_attest_test_config = Some(config);
232
233        self
234    }
235
236    /// Enable a synthnic for the VM.
237    ///
238    /// Uses a mana emulator and the paravisor if a paravisor is present.
239    pub fn with_nic(mut self) -> Self {
240        let endpoint =
241            net_backend_resources::consomme::ConsommeHandle { cidr: None }.into_resource();
242        if self.vtl2_settings.is_some() {
243            self.config.vpci_devices.push(VpciDeviceConfig {
244                vtl: DeviceVtl::Vtl2,
245                instance_id: MANA_INSTANCE,
246                resource: GdmaDeviceHandle {
247                    vports: vec![VportDefinition {
248                        mac_address: NIC_MAC_ADDRESS,
249                        endpoint,
250                    }],
251                }
252                .into_resource(),
253            });
254
255            self.vtl2_settings
256                .as_mut()
257                .unwrap()
258                .dynamic
259                .as_mut()
260                .unwrap()
261                .nic_devices
262                .push(vtl2_settings_proto::NicDeviceLegacy {
263                    instance_id: MANA_INSTANCE.to_string(),
264                    subordinate_instance_id: None,
265                    max_sub_channels: None,
266                });
267        } else {
268            const NETVSP_INSTANCE: guid::Guid = guid::guid!("c6c46cc3-9302-4344-b206-aef65e5bd0a2");
269            self.config.vmbus_devices.push((
270                DeviceVtl::Vtl0,
271                netvsp_resources::NetvspHandle {
272                    instance_id: NETVSP_INSTANCE,
273                    mac_address: NIC_MAC_ADDRESS,
274                    endpoint,
275                    max_queues: None,
276                }
277                .into_resource(),
278            ));
279        }
280
281        self
282    }
283
284    /// Specifies whether the UEFI frontpage app is enabled.
285    ///
286    /// If it is disabled, then the VM will shutdown if there are no configured
287    /// boot apps.
288    pub fn with_uefi_frontpage(mut self, enable_frontpage: bool) -> Self {
289        match self.config.load_mode {
290            LoadMode::Uefi {
291                ref mut disable_frontpage,
292                ..
293            } => {
294                *disable_frontpage = !enable_frontpage;
295            }
296            LoadMode::Igvm { .. } => {
297                let ged = self.ged.as_mut().expect("no GED to configure DPS");
298                match ged.firmware {
299                    get_resources::ged::GuestFirmwareConfig::Uefi {
300                        ref mut disable_frontpage,
301                        ..
302                    } => {
303                        *disable_frontpage = !enable_frontpage;
304                    }
305                    _ => {
306                        panic!("not a UEFI boot");
307                    }
308                }
309            }
310            _ => panic!("not a UEFI boot"),
311        }
312        self
313    }
314
315    /// Specifies whether the UEFI will always attempt a default boot
316    pub fn with_default_boot_always_attempt(mut self, val: bool) -> Self {
317        match self.config.load_mode {
318            LoadMode::Uefi {
319                ref mut default_boot_always_attempt,
320                ..
321            } => {
322                *default_boot_always_attempt = val;
323            }
324            LoadMode::Igvm { .. } => {
325                let ged = self.ged.as_mut().expect("no GED to configure DPS");
326                match ged.firmware {
327                    get_resources::ged::GuestFirmwareConfig::Uefi {
328                        ref mut default_boot_always_attempt,
329                        ..
330                    } => {
331                        *default_boot_always_attempt = val;
332                    }
333                    _ => {
334                        panic!("not a UEFI boot");
335                    }
336                }
337            }
338            _ => panic!("not a UEFI boot"),
339        }
340        self
341    }
342
343    /// Specifies an existing VMGS file to use
344    pub fn with_vmgs<T: IsTestVmgs>(mut self, vmgs: PetriVmgsResource<T>) -> Self {
345        let vmgs = match vmgs {
346            PetriVmgsResource::Disk(disk) => {
347                VmgsResource::Disk(memdiff_disk_from_artifact(&disk.erase()).expect("open VMGS"))
348            }
349            PetriVmgsResource::ReprovisionOnFailure(disk) => VmgsResource::ReprovisionOnFailure(
350                memdiff_disk_from_artifact(&disk.erase()).expect("open VMGS"),
351            ),
352            PetriVmgsResource::Reprovision(disk) => VmgsResource::Reprovision(
353                memdiff_disk_from_artifact(&disk.erase()).expect("open VMGS"),
354            ),
355            PetriVmgsResource::Ephemeral => VmgsResource::Ephemeral,
356        };
357
358        if self.firmware.is_openhcl() {
359            self.ged.as_mut().unwrap().vmgs = vmgs;
360        } else {
361            self.config.vmgs = Some(vmgs);
362        }
363        self
364    }
365
366    /// Add custom command line arguments to OpenHCL.
367    pub fn with_openhcl_command_line(mut self, additional_cmdline: &str) -> Self {
368        if !self.firmware.is_openhcl() {
369            panic!("Not an OpenHCL firmware.");
370        }
371        let LoadMode::Igvm { cmdline, .. } = &mut self.config.load_mode else {
372            unreachable!()
373        };
374        cmdline.push(' ');
375        cmdline.push_str(additional_cmdline);
376        self
377    }
378
379    /// Enable confidential filtering, even if the VM is not confidential.
380    pub fn with_confidential_filtering(self) -> Self {
381        if !self.firmware.is_openhcl() {
382            panic!("Confidential filtering is only supported for OpenHCL");
383        }
384        self.with_openhcl_command_line(&format!(
385            "{}=1 {}=0",
386            underhill_confidentiality::OPENHCL_CONFIDENTIAL_ENV_VAR_NAME,
387            underhill_confidentiality::OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME
388        ))
389    }
390
391    /// Load a custom OpenHCL firmware file.
392    pub fn with_custom_openhcl(mut self, artifact: ResolvedArtifact) -> Self {
393        let LoadMode::Igvm { file, .. } = &mut self.config.load_mode else {
394            panic!("Custom OpenHCL is only supported for OpenHCL firmware.")
395        };
396        *file = File::open(artifact)
397            .expect("Failed to open custom OpenHCL file")
398            .into();
399        self
400    }
401
402    /// Add custom VTL 2 settings.
403    // TODO: At some point we want to replace uses of this with nicer with_disk,
404    // with_nic, etc. methods.
405    pub fn with_custom_vtl2_settings(mut self, f: impl FnOnce(&mut Vtl2Settings)) -> Self {
406        f(self
407            .vtl2_settings
408            .as_mut()
409            .expect("Custom VTL 2 settings are only supported with OpenHCL."));
410        self
411    }
412
413    /// Load with the specified VTL2 relocation mode.
414    pub fn with_vtl2_relocation_mode(mut self, mode: Vtl2BaseAddressType) -> Self {
415        let LoadMode::Igvm {
416            vtl2_base_address, ..
417        } = &mut self.config.load_mode
418        else {
419            panic!("vtl2 relocation mode is only supported for OpenHCL firmware")
420        };
421        *vtl2_base_address = mode;
422        self
423    }
424
425    /// This is intended for special one-off use cases. As soon as something
426    /// is needed in multiple tests we should consider making it a supported
427    /// pattern.
428    pub fn with_custom_config(mut self, f: impl FnOnce(&mut Config)) -> Self {
429        f(&mut self.config);
430        self
431    }
432
433    /// Adds a file to the agent image.
434    pub fn with_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
435        self.resources.agent_image.add_file(name, artifact);
436        self
437    }
438
439    /// Adds a file to the OpenHCL agent image.
440    pub fn with_openhcl_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
441        self.resources
442            .openhcl_agent_image
443            .as_mut()
444            .unwrap()
445            .add_file(name, artifact);
446        self
447    }
448
449    /// Specifies whether VTL2 should be allowed to access VTL0 memory before it
450    /// sets any VTL protections.
451    ///
452    /// This is needed just for the TMK VMM, and only until it gains support for
453    /// setting VTL protections.
454    pub fn with_allow_early_vtl0_access(mut self, allow: bool) -> Self {
455        self.config
456            .hypervisor
457            .with_vtl2
458            .as_mut()
459            .unwrap()
460            .late_map_vtl0_memory =
461            (!allow).then_some(hvlite_defs::config::LateMapVtl0MemoryPolicy::InjectException);
462
463        self
464    }
465}