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    /// Enable a synthnic for the VM.
88    ///
89    /// Uses a mana emulator and the paravisor if a paravisor is present.
90    pub fn with_nic(mut self) -> Self {
91        let endpoint = net_backend_resources::consomme::ConsommeHandle {
92            cidr: None,
93            ports: Vec::new(),
94            recv: None,
95        }
96        .into_resource();
97        if let Some(vtl2_settings) = self.runtime_config.vtl2_settings.as_mut() {
98            self.config.vpci_devices.push(VpciDeviceConfig {
99                vtl: DeviceVtl::Vtl2,
100                instance_id: MANA_INSTANCE,
101                resource: GdmaDeviceHandle {
102                    vports: vec![VportDefinition {
103                        mac_address: NIC_MAC_ADDRESS,
104                        endpoint,
105                    }],
106                }
107                .into_resource(),
108                vnode: None,
109            });
110
111            vtl2_settings.dynamic.as_mut().unwrap().nic_devices.push(
112                vtl2_settings_proto::NicDeviceLegacy {
113                    instance_id: MANA_INSTANCE.to_string(),
114                    subordinate_instance_id: None,
115                    max_sub_channels: None,
116                },
117            );
118        } else {
119            const NETVSP_INSTANCE: Guid = guid::guid!("c6c46cc3-9302-4344-b206-aef65e5bd0a2");
120            self.config.vmbus_devices.push((
121                DeviceVtl::Vtl0,
122                netvsp_resources::NetvspHandle {
123                    instance_id: NETVSP_INSTANCE,
124                    mac_address: NIC_MAC_ADDRESS,
125                    endpoint,
126                    max_queues: None,
127                }
128                .into_resource(),
129            ));
130        }
131
132        self
133    }
134
135    /// Add a PCIe NIC to the VM using the MANA emulator.
136    pub fn with_pcie_nic(mut self, port_name: &str, mac_address: MacAddress) -> Self {
137        let endpoint = net_backend_resources::consomme::ConsommeHandle {
138            cidr: None,
139            ports: Vec::new(),
140            recv: None,
141        }
142        .into_resource();
143        self.config.pcie_devices.push(PcieDeviceConfig {
144            port_name: port_name.to_string(),
145            resource: GdmaDeviceHandle {
146                vports: vec![VportDefinition {
147                    mac_address,
148                    endpoint,
149                }],
150            }
151            .into_resource(),
152        });
153
154        self
155    }
156
157    /// Add a PCIe NVMe device to the VM using the NVMe emulator.
158    pub fn with_pcie_nvme(mut self, port_name: &str, subsystem_id: Guid) -> Self {
159        self.config.pcie_devices.push(PcieDeviceConfig {
160            port_name: port_name.to_string(),
161            resource: NvmeControllerHandle {
162                subsystem_id,
163                max_io_queues: 64,
164                msix_count: 64,
165                namespaces: vec![NamespaceDefinition {
166                    nsid: 1,
167                    disk: LayeredDiskHandle::single_layer(RamDiskLayerHandle {
168                        len: Some(1024 * 1024),
169                        sector_size: None,
170                    })
171                    .into_resource(),
172                    read_only: false,
173                }],
174                requests: None,
175            }
176            .into_resource(),
177        });
178
179        self
180    }
181
182    /// Enable a virtio-net NIC for the VM backed by Consomme.
183    ///
184    /// This exposes a virtio-net device on a PCIe root port, suitable for
185    /// guests running virtio drivers (e.g. Linux with UEFI boot).
186    pub fn with_virtio_nic(mut self, port_name: &str) -> Self {
187        let endpoint = net_backend_resources::consomme::ConsommeHandle {
188            cidr: None,
189            ports: Vec::new(),
190            recv: None,
191        }
192        .into_resource();
193
194        self.config.pcie_devices.push(PcieDeviceConfig {
195            port_name: port_name.to_string(),
196            resource: virtio_resources::VirtioPciDeviceHandle(
197                virtio_resources::net::VirtioNetHandle {
198                    max_queues: None,
199                    mac_address: NIC_MAC_ADDRESS,
200                    endpoint,
201                }
202                .into_resource(),
203            )
204            .into_resource(),
205        });
206
207        self
208    }
209
210    /// Add a virtio-net NIC with consomme and TCP port forwarding for
211    /// pipette. Used for Windows no-vmbus guests where virtio-vsock is
212    /// unavailable.
213    ///
214    /// This configures consomme to forward the pipette TCP port from the
215    /// host into the guest, so the petri framework can connect to the
216    /// pipette agent over TCP.
217    pub fn with_tcp_pipette_nic(mut self, port_name: &str) -> Self {
218        let (port_send, port_recv) = mesh::oneshot();
219        let endpoint = net_backend_resources::consomme::ConsommeHandle {
220            cidr: None,
221            ports: vec![net_backend_resources::consomme::HostPortConfig {
222                protocol: net_backend_resources::consomme::HostPortProtocol::Tcp,
223                host_address: Some(net_backend_resources::consomme::HostIpAddress::Ipv4(
224                    std::net::Ipv4Addr::LOCALHOST,
225                )),
226                host_port: net_backend_resources::consomme::HostPort::Dynamic(port_send),
227                guest_port: pipette_client::PIPETTE_PORT as u16,
228            }],
229            recv: None,
230        }
231        .into_resource();
232        self.config.pcie_devices.push(PcieDeviceConfig {
233            port_name: port_name.to_string(),
234            resource: virtio_resources::VirtioPciDeviceHandle(
235                virtio_resources::net::VirtioNetHandle {
236                    max_queues: None,
237                    mac_address: NIC_MAC_ADDRESS,
238                    endpoint,
239                }
240                .into_resource(),
241            )
242            .into_resource(),
243        });
244        self.resources.tcp_pipette_port = Some(port_recv);
245        self
246    }
247
248    /// Enable a synthnic for the VM backed by the Windows vmswitch
249    /// DirectIO (`-net dio`) backend.
250    ///
251    /// `switch_id`, when `None`, defaults to the Hyper-V Default Switch.
252    /// This requires the host to have Hyper-V installed and the chosen
253    /// switch available; tests that call this method should pre-resolve
254    /// a switch via [`super::find_switch`] (or an equivalent runtime
255    /// probe) and bail out with a clear error when the host does not
256    /// meet those requirements. The method itself panics if the switch
257    /// cannot be opened or a port cannot be created.
258    ///
259    /// The created vmswitch port handle is held in the petri (parent)
260    /// process for the lifetime of the VM. The kernel switch port object
261    /// is reference counted, so keeping the handle alive in this process
262    /// keeps the port usable from the child VMM process.
263    #[cfg(windows)]
264    pub fn with_dio_nic(mut self, switch_id: Option<Guid>) -> Self {
265        let switch_port_id = vmswitch::kernel::SwitchPortId {
266            switch: switch_id.unwrap_or(vmswitch::hcn::DEFAULT_SWITCH),
267            port: Guid::new_random(),
268        };
269        let _ = vmswitch::hcn::Network::open(&switch_port_id.switch)
270            .unwrap_or_else(|e| panic!("could not find switch {}: {e}", switch_port_id.switch));
271        let switch_port = vmswitch::kernel::SwitchPort::new(&switch_port_id)
272            .expect("failed to create vmswitch DIO port");
273        self.resources._switch_ports.push(switch_port);
274
275        let endpoint = net_backend_resources::dio::WindowsDirectIoHandle {
276            switch_port_id: net_backend_resources::dio::SwitchPortId {
277                switch: switch_port_id.switch,
278                port: switch_port_id.port,
279            },
280        }
281        .into_resource();
282
283        if let Some(vtl2_settings) = self.runtime_config.vtl2_settings.as_mut() {
284            self.config.vpci_devices.push(VpciDeviceConfig {
285                vtl: DeviceVtl::Vtl2,
286                instance_id: MANA_INSTANCE,
287                resource: GdmaDeviceHandle {
288                    vports: vec![VportDefinition {
289                        mac_address: NIC_MAC_ADDRESS,
290                        endpoint,
291                    }],
292                }
293                .into_resource(),
294                vnode: None,
295            });
296
297            vtl2_settings.dynamic.as_mut().unwrap().nic_devices.push(
298                vtl2_settings_proto::NicDeviceLegacy {
299                    instance_id: MANA_INSTANCE.to_string(),
300                    subordinate_instance_id: None,
301                    max_sub_channels: None,
302                },
303            );
304        } else {
305            const NETVSP_DIO_INSTANCE: Guid = guid::guid!("d1ff4c5a-1b3c-4f0d-8e10-1b9d8b1d1cee");
306            self.config.vmbus_devices.push((
307                DeviceVtl::Vtl0,
308                netvsp_resources::NetvspHandle {
309                    instance_id: NETVSP_DIO_INSTANCE,
310                    mac_address: NIC_MAC_ADDRESS,
311                    endpoint,
312                    max_queues: None,
313                }
314                .into_resource(),
315            ));
316        }
317
318        self
319    }
320
321    /// Load with the specified VTL2 relocation mode.
322    pub fn with_vtl2_relocation_mode(mut self, mode: Vtl2BaseAddressType) -> Self {
323        let LoadMode::Igvm {
324            vtl2_base_address, ..
325        } = &mut self.config.load_mode
326        else {
327            panic!("vtl2 relocation mode is only supported for OpenHCL firmware")
328        };
329        *vtl2_base_address = mode;
330        self
331    }
332
333    /// Use a file-backed memory region instead of anonymous RAM.
334    ///
335    /// The file at the given path will be created (or opened) and sized to
336    /// match the VM's configured memory. Guest memory is then backed by
337    /// this file, which persists across snapshot save/restore.
338    pub fn with_memory_backing_file(mut self, path: impl Into<std::path::PathBuf>) -> Self {
339        self.memory_backing_file = Some(path.into());
340        self
341    }
342
343    /// Use explicit hugetlb-backed guest memory.
344    pub fn with_hugepages(mut self, hugepage_size: Option<u64>) -> Self {
345        for node in &mut self.config.numa.nodes {
346            if let Some(mem) = &mut node.mem {
347                mem.hugepages = true;
348                mem.hugepage_size = hugepage_size;
349            }
350        }
351        self
352    }
353
354    /// Add a symmetric PCIe topology to the VM based on some basic scale factors
355    ///
356    /// All root ports are named according to their index within their parent
357    /// using the naming scheme `sXrcYrpZ`. For example, the third root port on
358    /// the fourth root complex in segment 0 would be named `s0rc3rp2`.
359    pub fn with_pcie_root_topology(
360        mut self,
361        segment_count: u64,
362        root_complex_per_segment: u64,
363        root_ports_per_root_complex: u64,
364    ) -> Self {
365        const LOW_MMIO_SIZE: u64 = 64 * 1024 * 1024; // 64 MB
366        const HIGH_MMIO_SIZE: u64 = 1024 * 1024 * 1024; // 1 GB
367
368        // Add the root complexes to the VM
369        for segment in 0..segment_count {
370            let bus_count_per_rc = 256 / root_complex_per_segment;
371            for rc_index_in_segment in 0..root_complex_per_segment {
372                let index = segment * root_complex_per_segment + rc_index_in_segment;
373                let name = format!("s{}rc{}", segment, rc_index_in_segment);
374
375                let start_bus = rc_index_in_segment * bus_count_per_rc;
376                let end_bus = start_bus + bus_count_per_rc - 1;
377
378                let ports = (0..root_ports_per_root_complex)
379                    .map(|i| PciePortConfig {
380                        name: format!("s{}rc{}rp{}", segment, rc_index_in_segment, i),
381                        devfn: None,
382                        hotplug: true,
383                        acs_capabilities_supported: Some(0),
384                        cxl: false,
385                    })
386                    .collect();
387
388                self.config.pcie_root_complexes.push(PcieRootComplexConfig {
389                    index: index.try_into().unwrap(),
390                    name,
391                    segment: segment.try_into().unwrap(),
392                    start_bus: start_bus.try_into().unwrap(),
393                    end_bus: end_bus.try_into().unwrap(),
394                    low_mmio: PcieMmioRangeConfig::Dynamic {
395                        size: LOW_MMIO_SIZE,
396                    },
397                    high_mmio: PcieMmioRangeConfig::Dynamic {
398                        size: HIGH_MMIO_SIZE,
399                    },
400                    cxl: None,
401                    ports,
402                    iommu: None,
403                    vnode: None,
404                    preserve_bars: false,
405                });
406            }
407        }
408
409        self
410    }
411
412    /// Add a PCIe switch to the VM.
413    pub fn with_pcie_switch(
414        mut self,
415        port_name: &str,
416        switch_name: &str,
417        port_count: u8,
418        hotplug: bool,
419    ) -> Self {
420        self.config.pcie_switches.push(PcieSwitchConfig {
421            name: switch_name.to_string(),
422            parent_port: port_name.to_string(),
423            ports: (0..port_count)
424                .map(|i| PciePortConfig {
425                    name: format!("{switch_name}-downstream-{i}"),
426                    devfn: None,
427                    hotplug,
428                    acs_capabilities_supported: Some(0),
429                    cxl: false,
430                })
431                .collect(),
432        });
433        self
434    }
435
436    /// Enable SMMUv3 IOMMU on the specified root complexes (aarch64 only).
437    ///
438    /// Each name must match a root complex added via
439    /// [`with_pcie_root_topology`](Self::with_pcie_root_topology). The SMMU
440    /// provides stage 1 IOVA translation for devices behind those root
441    /// complexes.
442    pub fn with_smmu(mut self, rc_names: &[&str]) -> Self {
443        for name in rc_names {
444            self.pending_iommu
445                .push((name.to_string(), PcieIommuConfig::Smmu));
446        }
447        self
448    }
449
450    /// Enable AMD IOMMU (AMD-Vi) on the specified root complexes.
451    ///
452    /// Each name must match a root complex added via
453    /// [`with_pcie_root_topology`](Self::with_pcie_root_topology). The IOMMU
454    /// appears at device 0 function 0 on each listed root complex; PCIe
455    /// devices behind those root complexes have DMA translated through
456    /// guest-programmed page tables and MSIs remapped through the interrupt
457    /// remapping table.
458    pub fn with_amd_iommu(mut self, rc_names: &[&str]) -> Self {
459        for name in rc_names {
460            self.pending_iommu
461                .push((name.to_string(), PcieIommuConfig::AmdVi));
462        }
463        self
464    }
465
466    /// Enable Intel VT-d IOMMU on the specified root complexes.
467    ///
468    /// Each name must match a root complex added via
469    /// [`with_pcie_root_topology`](Self::with_pcie_root_topology). The IOMMU
470    /// is a platform device discovered via the ACPI DMAR table; PCIe devices
471    /// behind those root complexes have DMA translated through
472    /// guest-programmed page tables and MSIs remapped through the interrupt
473    /// remapping table.
474    pub fn with_intel_vtd(mut self, rc_names: &[&str]) -> Self {
475        for name in rc_names {
476            self.pending_iommu
477                .push((name.to_string(), PcieIommuConfig::IntelVtd));
478        }
479        self
480    }
481
482    /// This is intended for special one-off use cases. As soon as something
483    /// is needed in multiple tests we should consider making it a supported
484    /// pattern.
485    pub fn with_custom_config(mut self, f: impl FnOnce(&mut Config)) -> Self {
486        f(&mut self.config);
487        self
488    }
489
490    /// Specifies whether VTL2 should be allowed to access VTL0 memory before it
491    /// sets any VTL protections.
492    ///
493    /// This is needed just for the TMK VMM, and only until it gains support for
494    /// setting VTL protections.
495    pub fn with_allow_early_vtl0_access(mut self, allow: bool) -> Self {
496        self.config
497            .hypervisor
498            .with_vtl2
499            .as_mut()
500            .unwrap()
501            .late_map_vtl0_memory =
502            (!allow).then_some(openvmm_defs::config::LateMapVtl0MemoryPolicy::InjectException);
503
504        self
505    }
506}