Skip to main content

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
6// TODO: Delete all modification functions that are not backend-specific
7// from this file, add necessary settings to the backend-agnostic
8// `PetriVmConfig`, and add corresponding functions to `PetriVmBuilder`.
9
10use super::MANA_INSTANCE;
11use super::NIC_MAC_ADDRESS;
12use super::PetriVmConfigOpenVmm;
13use chipset_resources::battery::BatteryDeviceHandleX64;
14use chipset_resources::battery::HostBatteryUpdate;
15use disk_backend_resources::LayeredDiskHandle;
16use disk_backend_resources::layer::RamDiskLayerHandle;
17use gdma_resources::GdmaDeviceHandle;
18use gdma_resources::VportDefinition;
19use get_resources::ged::IgvmAttestTestConfig;
20use guid::Guid;
21use net_backend_resources::mac_address::MacAddress;
22use nvme_resources::NamespaceDefinition;
23use nvme_resources::NvmeControllerHandle;
24use openvmm_defs::config::Config;
25use openvmm_defs::config::DeviceVtl;
26use openvmm_defs::config::LoadMode;
27use openvmm_defs::config::PcieDeviceConfig;
28use openvmm_defs::config::PcieIommuConfig;
29use openvmm_defs::config::PcieMmioRangeConfig;
30use openvmm_defs::config::PciePortConfig;
31use openvmm_defs::config::PcieRootComplexConfig;
32use openvmm_defs::config::PcieSwitchConfig;
33use openvmm_defs::config::VpciDeviceConfig;
34use openvmm_defs::config::Vtl2BaseAddressType;
35use vm_resource::IntoResource;
36use vmotherboard::ChipsetDeviceHandle;
37
38impl PetriVmConfigOpenVmm {
39    /// Enable the VTL0 alias map.
40    // TODO: Remove once #912 is fixed.
41    pub fn with_vtl0_alias_map(mut self) -> Self {
42        self.config
43            .hypervisor
44            .with_vtl2
45            .as_mut()
46            .expect("Not an openhcl config.")
47            .vtl0_alias_map = true;
48        self
49    }
50
51    /// Enable the battery for the VM.
52    pub fn with_battery(mut self) -> Self {
53        if self.resources.properties.is_openhcl {
54            self.ged.as_mut().unwrap().enable_battery = true;
55        } else {
56            self.config.chipset_devices.push(ChipsetDeviceHandle {
57                name: "battery".to_string(),
58                resource: BatteryDeviceHandleX64 {
59                    battery_status_recv: {
60                        let (tx, rx) = mesh::channel();
61                        tx.send(HostBatteryUpdate::default_present());
62                        rx
63                    },
64                }
65                .into_resource(),
66            });
67            if let LoadMode::Uefi { enable_battery, .. } = &mut self.config.load_mode {
68                *enable_battery = true;
69            }
70        }
71        self
72    }
73
74    /// Set test config for the GED's IGVM attest request handler
75    pub fn with_igvm_attest_test_config(mut self, config: IgvmAttestTestConfig) -> Self {
76        if !self.resources.properties.is_openhcl {
77            panic!("IGVM Attest test config is only supported for OpenHCL.")
78        };
79
80        let ged = self.ged.as_mut().expect("No GED to configure TPM");
81
82        ged.igvm_attest_test_config = Some(config);
83
84        self
85    }
86
87    /// Override the SMBIOS identity delivered to the guest, regardless of how
88    /// the VM is loaded.
89    ///
90    /// For OpenHCL the identity is forwarded to the paravisor over the Guest
91    /// Emulation Transport (GET), which synthesizes the guest's DMI tables from
92    /// it. For direct OpenVMM boot (Linux direct, UEFI, or PCAT) it is applied
93    /// to the loader's SMBIOS config. Each load path honors only the subset of
94    /// fields it can express and fails closed on the rest.
95    pub fn with_smbios(mut self, f: impl FnOnce(&mut smbios_defs::SmbiosConfig)) -> Self {
96        if self.resources.properties.is_openhcl {
97            let ged = self.ged.as_mut().expect("OpenHCL config must have a GED.");
98            f(&mut ged.smbios);
99        } else {
100            let smbios = match &mut self.config.load_mode {
101                LoadMode::Linux { smbios, .. }
102                | LoadMode::Uefi { smbios, .. }
103                | LoadMode::Pcat { smbios, .. } => &mut **smbios,
104                LoadMode::Igvm { .. } | LoadMode::None => {
105                    panic!("SMBIOS configuration is not supported for this load mode.")
106                }
107            };
108            f(smbios);
109        }
110        self
111    }
112
113    /// Enable a synthnic for the VM.
114    ///
115    /// Uses a mana emulator and the paravisor if a paravisor is present.
116    pub fn with_nic(mut self) -> Self {
117        let endpoint = net_backend_resources::consomme::ConsommeHandle {
118            cidr: None,
119            ports: Vec::new(),
120            recv: None,
121        }
122        .into_resource();
123        if let Some(vtl2_settings) = self.runtime_config.vtl2_settings.as_mut() {
124            self.config.vpci_devices.push(VpciDeviceConfig {
125                vtl: DeviceVtl::Vtl2,
126                instance_id: MANA_INSTANCE,
127                resource: GdmaDeviceHandle {
128                    vports: vec![VportDefinition {
129                        mac_address: NIC_MAC_ADDRESS,
130                        endpoint,
131                    }],
132                }
133                .into_resource(),
134                vnode: None,
135            });
136
137            vtl2_settings.dynamic.as_mut().unwrap().nic_devices.push(
138                vtl2_settings_proto::NicDeviceLegacy {
139                    instance_id: MANA_INSTANCE.to_string(),
140                    subordinate_instance_id: None,
141                    max_sub_channels: None,
142                },
143            );
144        } else {
145            const NETVSP_INSTANCE: Guid = guid::guid!("c6c46cc3-9302-4344-b206-aef65e5bd0a2");
146            self.config.vmbus_devices.push((
147                DeviceVtl::Vtl0,
148                netvsp_resources::NetvspHandle {
149                    instance_id: NETVSP_INSTANCE,
150                    mac_address: NIC_MAC_ADDRESS,
151                    endpoint,
152                    max_queues: None,
153                }
154                .into_resource(),
155            ));
156        }
157
158        self
159    }
160
161    /// Add a PCIe NIC to the VM using the MANA emulator.
162    pub fn with_pcie_nic(mut self, port_name: &str, mac_address: MacAddress) -> Self {
163        let endpoint = net_backend_resources::consomme::ConsommeHandle {
164            cidr: None,
165            ports: Vec::new(),
166            recv: None,
167        }
168        .into_resource();
169        self.config.pcie_devices.push(PcieDeviceConfig {
170            port_name: port_name.to_string(),
171            resource: GdmaDeviceHandle {
172                vports: vec![VportDefinition {
173                    mac_address,
174                    endpoint,
175                }],
176            }
177            .into_resource(),
178        });
179
180        self
181    }
182
183    /// Add a PCIe NVMe device to the VM using the NVMe emulator.
184    pub fn with_pcie_nvme(mut self, port_name: &str, subsystem_id: Guid) -> Self {
185        self.config.pcie_devices.push(PcieDeviceConfig {
186            port_name: port_name.to_string(),
187            resource: NvmeControllerHandle {
188                subsystem_id,
189                max_io_queues: 64,
190                msix_count: 64,
191                namespaces: vec![NamespaceDefinition {
192                    nsid: 1,
193                    disk: LayeredDiskHandle::single_layer(RamDiskLayerHandle {
194                        len: Some(1024 * 1024),
195                        sector_size: None,
196                    })
197                    .into_resource(),
198                    read_only: false,
199                }],
200                requests: None,
201            }
202            .into_resource(),
203        });
204
205        self
206    }
207
208    /// Enable a virtio-net NIC for the VM backed by Consomme.
209    ///
210    /// This exposes a virtio-net device on a PCIe root port, suitable for
211    /// guests running virtio drivers (e.g. Linux with UEFI boot).
212    pub fn with_virtio_nic(mut self, port_name: &str, mac_address: MacAddress) -> Self {
213        let endpoint = net_backend_resources::consomme::ConsommeHandle {
214            cidr: None,
215            ports: Vec::new(),
216            recv: None,
217        }
218        .into_resource();
219
220        self.config.pcie_devices.push(PcieDeviceConfig {
221            port_name: port_name.to_string(),
222            resource: virtio_resources::VirtioPciDeviceHandle(
223                virtio_resources::net::VirtioNetHandle {
224                    max_queues: None,
225                    mac_address,
226                    endpoint,
227                }
228                .into_resource(),
229            )
230            .into_resource(),
231        });
232
233        self
234    }
235
236    /// Add a virtio-net NIC with consomme and TCP port forwarding for
237    /// pipette. Used for Windows no-vmbus guests where virtio-vsock is
238    /// unavailable.
239    ///
240    /// This configures consomme to forward the pipette TCP port from the
241    /// host into the guest, so the petri framework can connect to the
242    /// pipette agent over TCP.
243    pub fn with_tcp_pipette_nic(mut self, port_name: &str, mac_address: MacAddress) -> Self {
244        let (port_send, port_recv) = mesh::oneshot();
245        let endpoint = net_backend_resources::consomme::ConsommeHandle {
246            cidr: None,
247            ports: vec![net_backend_resources::consomme::HostPortConfig {
248                protocol: net_backend_resources::consomme::HostPortProtocol::Tcp,
249                host_address: Some(net_backend_resources::consomme::HostIpAddress::Ipv4(
250                    std::net::Ipv4Addr::LOCALHOST,
251                )),
252                host_port: net_backend_resources::consomme::HostPort::Dynamic(port_send),
253                guest_port: pipette_client::PIPETTE_PORT as u16,
254            }],
255            recv: None,
256        }
257        .into_resource();
258        self.config.pcie_devices.push(PcieDeviceConfig {
259            port_name: port_name.to_string(),
260            resource: virtio_resources::VirtioPciDeviceHandle(
261                virtio_resources::net::VirtioNetHandle {
262                    max_queues: None,
263                    mac_address,
264                    endpoint,
265                }
266                .into_resource(),
267            )
268            .into_resource(),
269        });
270        self.resources.tcp_pipette_port = Some(port_recv);
271        self
272    }
273
274    /// Request nested virtualization support from the host hypervisor.
275    pub fn with_nested_virt(mut self) -> Self {
276        self.config.hypervisor.nested_virt = true;
277        self
278    }
279
280    /// Enable a synthnic for the VM backed by the Windows vmswitch
281    /// DirectIO (`-net dio`) backend.
282    ///
283    /// `switch_id`, when `None`, defaults to the Hyper-V Default Switch.
284    /// This requires the host to have Hyper-V installed and the chosen
285    /// switch available; tests that call this method should pre-resolve
286    /// a switch via [`super::find_switch`] (or an equivalent runtime
287    /// probe) and bail out with a clear error when the host does not
288    /// meet those requirements. The method itself panics if the switch
289    /// cannot be opened or a port cannot be created.
290    ///
291    /// The created vmswitch port handle is held in the petri (parent)
292    /// process for the lifetime of the VM. The kernel switch port object
293    /// is reference counted, so keeping the handle alive in this process
294    /// keeps the port usable from the child VMM process.
295    #[cfg(windows)]
296    pub fn with_dio_nic(mut self, switch_id: Option<Guid>) -> Self {
297        let switch_port_id = vmswitch::kernel::SwitchPortId {
298            switch: switch_id.unwrap_or(vmswitch::hcn::DEFAULT_SWITCH),
299            port: Guid::new_random(),
300        };
301        let _ = vmswitch::hcn::Network::open(&switch_port_id.switch)
302            .unwrap_or_else(|e| panic!("could not find switch {}: {e}", switch_port_id.switch));
303        let switch_port = vmswitch::kernel::SwitchPort::new(&switch_port_id)
304            .expect("failed to create vmswitch DIO port");
305        self.resources._switch_ports.push(switch_port);
306
307        let endpoint = net_backend_resources::dio::WindowsDirectIoHandle {
308            switch_port_id: net_backend_resources::dio::SwitchPortId {
309                switch: switch_port_id.switch,
310                port: switch_port_id.port,
311            },
312        }
313        .into_resource();
314
315        if let Some(vtl2_settings) = self.runtime_config.vtl2_settings.as_mut() {
316            self.config.vpci_devices.push(VpciDeviceConfig {
317                vtl: DeviceVtl::Vtl2,
318                instance_id: MANA_INSTANCE,
319                resource: GdmaDeviceHandle {
320                    vports: vec![VportDefinition {
321                        mac_address: NIC_MAC_ADDRESS,
322                        endpoint,
323                    }],
324                }
325                .into_resource(),
326                vnode: None,
327            });
328
329            vtl2_settings.dynamic.as_mut().unwrap().nic_devices.push(
330                vtl2_settings_proto::NicDeviceLegacy {
331                    instance_id: MANA_INSTANCE.to_string(),
332                    subordinate_instance_id: None,
333                    max_sub_channels: None,
334                },
335            );
336        } else {
337            const NETVSP_DIO_INSTANCE: Guid = guid::guid!("d1ff4c5a-1b3c-4f0d-8e10-1b9d8b1d1cee");
338            self.config.vmbus_devices.push((
339                DeviceVtl::Vtl0,
340                netvsp_resources::NetvspHandle {
341                    instance_id: NETVSP_DIO_INSTANCE,
342                    mac_address: NIC_MAC_ADDRESS,
343                    endpoint,
344                    max_queues: None,
345                }
346                .into_resource(),
347            ));
348        }
349
350        self
351    }
352
353    /// Load with the specified VTL2 relocation mode.
354    pub fn with_vtl2_relocation_mode(mut self, mode: Vtl2BaseAddressType) -> Self {
355        let LoadMode::Igvm {
356            vtl2_base_address, ..
357        } = &mut self.config.load_mode
358        else {
359            panic!("vtl2 relocation mode is only supported for OpenHCL firmware")
360        };
361        *vtl2_base_address = mode;
362        self
363    }
364
365    /// Use a file-backed memory region instead of anonymous RAM.
366    ///
367    /// The file at the given path will be created (or opened) and sized to
368    /// match the VM's configured memory. Guest memory is then backed by
369    /// this file, which persists across snapshot save/restore.
370    ///
371    /// This forces shared (non-private) memory, since a file-backed mapping
372    /// is incompatible with private anonymous RAM. Panics if the caller
373    /// explicitly requested private memory via
374    /// [`MemoryConfig::private_memory`](crate::MemoryConfig::private_memory),
375    /// rather than silently downgrading it.
376    pub fn with_memory_backing_file(mut self, path: impl Into<std::path::PathBuf>) -> Self {
377        assert_ne!(
378            self.requested_private_memory,
379            Some(true),
380            "with_memory_backing_file forces shared memory, which conflicts with \
381             the explicitly requested private memory"
382        );
383        self.memory_backing_file = Some(path.into());
384        for node in &mut self.config.numa.nodes {
385            if let Some(mem) = &mut node.mem {
386                mem.private_memory = false;
387            }
388        }
389        self
390    }
391
392    /// Use explicit hugetlb-backed guest memory.
393    ///
394    /// This forces shared (non-private) memory, since hugetlb backing
395    /// requires a file-backed mapping rather than private anonymous RAM.
396    /// Panics if the caller explicitly requested private memory via
397    /// [`MemoryConfig::private_memory`](crate::MemoryConfig::private_memory),
398    /// rather than silently downgrading it.
399    pub fn with_hugepages(mut self, hugepage_size: Option<u64>) -> Self {
400        assert_ne!(
401            self.requested_private_memory,
402            Some(true),
403            "with_hugepages forces shared memory, which conflicts with the \
404             explicitly requested private memory"
405        );
406        for node in &mut self.config.numa.nodes {
407            if let Some(mem) = &mut node.mem {
408                mem.hugepages = true;
409                mem.hugepage_size = hugepage_size;
410                mem.private_memory = false;
411            }
412        }
413        self
414    }
415
416    /// Add a symmetric PCIe topology to the VM based on some basic scale factors
417    ///
418    /// All root ports are named according to their index within their parent
419    /// using the naming scheme `sXrcYrpZ`. For example, the third root port on
420    /// the fourth root complex in segment 0 would be named `s0rc3rp2`.
421    ///
422    /// This may be called multiple times to build asymmetric topologies (e.g. a
423    /// different number of root complexes per segment). Each call appends its
424    /// root complexes to segments numbered after any added by previous calls,
425    /// so the segment numbers in the `sXrcY` names continue from where the last
426    /// call left off.
427    pub fn with_pcie_root_topology(
428        mut self,
429        segment_count: u64,
430        root_complex_per_segment: u64,
431        root_ports_per_root_complex: u64,
432    ) -> Self {
433        const LOW_MMIO_SIZE: u64 = 64 * 1024 * 1024; // 64 MB
434        const HIGH_MMIO_SIZE: u64 = 1024 * 1024 * 1024; // 1 GB
435
436        // Offset the segments and global indices added by this call so that it
437        // can be called multiple times. New segments are numbered after any
438        // existing ones, and the global index continues from the existing
439        // root complex count.
440        let segment_base = self
441            .config
442            .pcie_root_complexes
443            .iter()
444            .map(|rc| u64::from(rc.segment) + 1)
445            .max()
446            .unwrap_or(0);
447        let index_base = self.config.pcie_root_complexes.len() as u64;
448
449        // Add the root complexes to the VM
450        for segment_offset in 0..segment_count {
451            let segment = segment_base + segment_offset;
452            let bus_count_per_rc = 256 / root_complex_per_segment;
453            for rc_index_in_segment in 0..root_complex_per_segment {
454                let index =
455                    index_base + segment_offset * root_complex_per_segment + rc_index_in_segment;
456                let name = format!("s{}rc{}", segment, rc_index_in_segment);
457
458                let start_bus = rc_index_in_segment * bus_count_per_rc;
459                let end_bus = start_bus + bus_count_per_rc - 1;
460
461                let ports = (0..root_ports_per_root_complex)
462                    .map(|i| PciePortConfig {
463                        name: format!("s{}rc{}rp{}", segment, rc_index_in_segment, i),
464                        devfn: None,
465                        hotplug: true,
466                        acs_capabilities_supported: Some(0),
467                        cxl: false,
468                        pasid: false,
469                    })
470                    .collect();
471
472                self.config.pcie_root_complexes.push(PcieRootComplexConfig {
473                    index: index.try_into().unwrap(),
474                    name,
475                    segment: segment.try_into().unwrap(),
476                    start_bus: start_bus.try_into().unwrap(),
477                    end_bus: end_bus.try_into().unwrap(),
478                    low_mmio: PcieMmioRangeConfig::Dynamic {
479                        size: LOW_MMIO_SIZE,
480                    },
481                    high_mmio: PcieMmioRangeConfig::Dynamic {
482                        size: HIGH_MMIO_SIZE,
483                    },
484                    cxl: None,
485                    ports,
486                    iommu: None,
487                    vnode: None,
488                    preserve_bars: false,
489                });
490            }
491        }
492
493        self
494    }
495
496    /// Add a PCIe switch to the VM.
497    pub fn with_pcie_switch(
498        mut self,
499        port_name: &str,
500        switch_name: &str,
501        port_count: u8,
502        hotplug: bool,
503    ) -> Self {
504        self.config.pcie_switches.push(PcieSwitchConfig {
505            name: switch_name.to_string(),
506            parent_port: port_name.to_string(),
507            ports: (0..port_count)
508                .map(|i| PciePortConfig {
509                    name: format!("{switch_name}-downstream-{i}"),
510                    devfn: None,
511                    hotplug,
512                    acs_capabilities_supported: Some(0),
513                    cxl: false,
514                    pasid: false,
515                })
516                .collect(),
517        });
518        self
519    }
520
521    /// Enable SMMUv3 IOMMU on the specified root complexes (aarch64 only).
522    ///
523    /// Each name must match a root complex added via
524    /// [`with_pcie_root_topology`](Self::with_pcie_root_topology). The SMMU
525    /// provides stage 1 IOVA translation for devices behind those root
526    /// complexes.
527    pub fn with_smmu(mut self, rc_names: &[&str]) -> Self {
528        for name in rc_names {
529            self.pending_iommu.push((
530                name.to_string(),
531                PcieIommuConfig::Smmu {
532                    accel: false,
533                    oas: openvmm_defs::config::SmmuOas::Auto,
534                },
535            ));
536        }
537        self
538    }
539
540    /// Enable an accelerated (iommufd-nested) SMMUv3 on the specified root
541    /// complexes (aarch64 only).
542    ///
543    /// Like [`with_smmu`](Self::with_smmu), but the SMMU programs the host
544    /// IOMMU for hardware nested stage-1 translation, so VFIO devices behind
545    /// these root complexes are permitted (and their guest-programmed stage-1
546    /// tables are honored via a host nested HWPT). Requires a host SMMU that
547    /// supports iommufd nesting.
548    pub fn with_smmu_accel(mut self, rc_names: &[&str]) -> Self {
549        for name in rc_names {
550            self.pending_iommu.push((
551                name.to_string(),
552                PcieIommuConfig::Smmu {
553                    accel: true,
554                    oas: openvmm_defs::config::SmmuOas::Auto,
555                },
556            ));
557        }
558        self
559    }
560
561    /// Enable AMD IOMMU (AMD-Vi) on the specified root complexes.
562    ///
563    /// Each name must match a root complex added via
564    /// [`with_pcie_root_topology`](Self::with_pcie_root_topology). The IOMMU
565    /// appears at device 0 function 0 on each listed root complex; PCIe
566    /// devices behind those root complexes have DMA translated through
567    /// guest-programmed page tables and MSIs remapped through the interrupt
568    /// remapping table.
569    pub fn with_amd_iommu(mut self, rc_names: &[&str]) -> Self {
570        for name in rc_names {
571            self.pending_iommu
572                .push((name.to_string(), PcieIommuConfig::AmdVi));
573        }
574        self
575    }
576
577    /// Enable Intel VT-d IOMMU on the specified root complexes.
578    ///
579    /// Each name must match a root complex added via
580    /// [`with_pcie_root_topology`](Self::with_pcie_root_topology). The IOMMU
581    /// is a platform device discovered via the ACPI DMAR table; PCIe devices
582    /// behind those root complexes have DMA translated through
583    /// guest-programmed page tables and MSIs remapped through the interrupt
584    /// remapping table.
585    pub fn with_intel_vtd(mut self, rc_names: &[&str]) -> Self {
586        for name in rc_names {
587            self.pending_iommu
588                .push((name.to_string(), PcieIommuConfig::IntelVtd));
589        }
590        self
591    }
592
593    /// This is intended for special one-off use cases. As soon as something
594    /// is needed in multiple tests we should consider making it a supported
595    /// pattern.
596    pub fn with_custom_config(mut self, f: impl FnOnce(&mut Config)) -> Self {
597        f(&mut self.config);
598        self
599    }
600
601    /// Specifies whether VTL2 should be allowed to access VTL0 memory before it
602    /// sets any VTL protections.
603    ///
604    /// This is needed just for the TMK VMM, and only until it gains support for
605    /// setting VTL protections.
606    pub fn with_allow_early_vtl0_access(mut self, allow: bool) -> Self {
607        self.config
608            .hypervisor
609            .with_vtl2
610            .as_mut()
611            .unwrap()
612            .late_map_vtl0_memory =
613            (!allow).then_some(openvmm_defs::config::LateMapVtl0MemoryPolicy::InjectException);
614
615        self
616    }
617}