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, mac_address: MacAddress) -> 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,
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, mac_address: MacAddress) -> 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,
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    /// Request nested virtualization support from the host hypervisor.
249    pub fn with_nested_virt(mut self) -> Self {
250        self.config.hypervisor.nested_virt = true;
251        self
252    }
253
254    /// Enable a synthnic for the VM backed by the Windows vmswitch
255    /// DirectIO (`-net dio`) backend.
256    ///
257    /// `switch_id`, when `None`, defaults to the Hyper-V Default Switch.
258    /// This requires the host to have Hyper-V installed and the chosen
259    /// switch available; tests that call this method should pre-resolve
260    /// a switch via [`super::find_switch`] (or an equivalent runtime
261    /// probe) and bail out with a clear error when the host does not
262    /// meet those requirements. The method itself panics if the switch
263    /// cannot be opened or a port cannot be created.
264    ///
265    /// The created vmswitch port handle is held in the petri (parent)
266    /// process for the lifetime of the VM. The kernel switch port object
267    /// is reference counted, so keeping the handle alive in this process
268    /// keeps the port usable from the child VMM process.
269    #[cfg(windows)]
270    pub fn with_dio_nic(mut self, switch_id: Option<Guid>) -> Self {
271        let switch_port_id = vmswitch::kernel::SwitchPortId {
272            switch: switch_id.unwrap_or(vmswitch::hcn::DEFAULT_SWITCH),
273            port: Guid::new_random(),
274        };
275        let _ = vmswitch::hcn::Network::open(&switch_port_id.switch)
276            .unwrap_or_else(|e| panic!("could not find switch {}: {e}", switch_port_id.switch));
277        let switch_port = vmswitch::kernel::SwitchPort::new(&switch_port_id)
278            .expect("failed to create vmswitch DIO port");
279        self.resources._switch_ports.push(switch_port);
280
281        let endpoint = net_backend_resources::dio::WindowsDirectIoHandle {
282            switch_port_id: net_backend_resources::dio::SwitchPortId {
283                switch: switch_port_id.switch,
284                port: switch_port_id.port,
285            },
286        }
287        .into_resource();
288
289        if let Some(vtl2_settings) = self.runtime_config.vtl2_settings.as_mut() {
290            self.config.vpci_devices.push(VpciDeviceConfig {
291                vtl: DeviceVtl::Vtl2,
292                instance_id: MANA_INSTANCE,
293                resource: GdmaDeviceHandle {
294                    vports: vec![VportDefinition {
295                        mac_address: NIC_MAC_ADDRESS,
296                        endpoint,
297                    }],
298                }
299                .into_resource(),
300                vnode: None,
301            });
302
303            vtl2_settings.dynamic.as_mut().unwrap().nic_devices.push(
304                vtl2_settings_proto::NicDeviceLegacy {
305                    instance_id: MANA_INSTANCE.to_string(),
306                    subordinate_instance_id: None,
307                    max_sub_channels: None,
308                },
309            );
310        } else {
311            const NETVSP_DIO_INSTANCE: Guid = guid::guid!("d1ff4c5a-1b3c-4f0d-8e10-1b9d8b1d1cee");
312            self.config.vmbus_devices.push((
313                DeviceVtl::Vtl0,
314                netvsp_resources::NetvspHandle {
315                    instance_id: NETVSP_DIO_INSTANCE,
316                    mac_address: NIC_MAC_ADDRESS,
317                    endpoint,
318                    max_queues: None,
319                }
320                .into_resource(),
321            ));
322        }
323
324        self
325    }
326
327    /// Load with the specified VTL2 relocation mode.
328    pub fn with_vtl2_relocation_mode(mut self, mode: Vtl2BaseAddressType) -> Self {
329        let LoadMode::Igvm {
330            vtl2_base_address, ..
331        } = &mut self.config.load_mode
332        else {
333            panic!("vtl2 relocation mode is only supported for OpenHCL firmware")
334        };
335        *vtl2_base_address = mode;
336        self
337    }
338
339    /// Use a file-backed memory region instead of anonymous RAM.
340    ///
341    /// The file at the given path will be created (or opened) and sized to
342    /// match the VM's configured memory. Guest memory is then backed by
343    /// this file, which persists across snapshot save/restore.
344    ///
345    /// This forces shared (non-private) memory, since a file-backed mapping
346    /// is incompatible with private anonymous RAM. Panics if the caller
347    /// explicitly requested private memory via
348    /// [`MemoryConfig::private_memory`](crate::MemoryConfig::private_memory),
349    /// rather than silently downgrading it.
350    pub fn with_memory_backing_file(mut self, path: impl Into<std::path::PathBuf>) -> Self {
351        assert_ne!(
352            self.requested_private_memory,
353            Some(true),
354            "with_memory_backing_file forces shared memory, which conflicts with \
355             the explicitly requested private memory"
356        );
357        self.memory_backing_file = Some(path.into());
358        for node in &mut self.config.numa.nodes {
359            if let Some(mem) = &mut node.mem {
360                mem.private_memory = false;
361            }
362        }
363        self
364    }
365
366    /// Use explicit hugetlb-backed guest memory.
367    ///
368    /// This forces shared (non-private) memory, since hugetlb backing
369    /// requires a file-backed mapping rather than private anonymous RAM.
370    /// Panics if the caller explicitly requested private memory via
371    /// [`MemoryConfig::private_memory`](crate::MemoryConfig::private_memory),
372    /// rather than silently downgrading it.
373    pub fn with_hugepages(mut self, hugepage_size: Option<u64>) -> Self {
374        assert_ne!(
375            self.requested_private_memory,
376            Some(true),
377            "with_hugepages forces shared memory, which conflicts with the \
378             explicitly requested private memory"
379        );
380        for node in &mut self.config.numa.nodes {
381            if let Some(mem) = &mut node.mem {
382                mem.hugepages = true;
383                mem.hugepage_size = hugepage_size;
384                mem.private_memory = false;
385            }
386        }
387        self
388    }
389
390    /// Add a symmetric PCIe topology to the VM based on some basic scale factors
391    ///
392    /// All root ports are named according to their index within their parent
393    /// using the naming scheme `sXrcYrpZ`. For example, the third root port on
394    /// the fourth root complex in segment 0 would be named `s0rc3rp2`.
395    ///
396    /// This may be called multiple times to build asymmetric topologies (e.g. a
397    /// different number of root complexes per segment). Each call appends its
398    /// root complexes to segments numbered after any added by previous calls,
399    /// so the segment numbers in the `sXrcY` names continue from where the last
400    /// call left off.
401    pub fn with_pcie_root_topology(
402        mut self,
403        segment_count: u64,
404        root_complex_per_segment: u64,
405        root_ports_per_root_complex: u64,
406    ) -> Self {
407        const LOW_MMIO_SIZE: u64 = 64 * 1024 * 1024; // 64 MB
408        const HIGH_MMIO_SIZE: u64 = 1024 * 1024 * 1024; // 1 GB
409
410        // Offset the segments and global indices added by this call so that it
411        // can be called multiple times. New segments are numbered after any
412        // existing ones, and the global index continues from the existing
413        // root complex count.
414        let segment_base = self
415            .config
416            .pcie_root_complexes
417            .iter()
418            .map(|rc| u64::from(rc.segment) + 1)
419            .max()
420            .unwrap_or(0);
421        let index_base = self.config.pcie_root_complexes.len() as u64;
422
423        // Add the root complexes to the VM
424        for segment_offset in 0..segment_count {
425            let segment = segment_base + segment_offset;
426            let bus_count_per_rc = 256 / root_complex_per_segment;
427            for rc_index_in_segment in 0..root_complex_per_segment {
428                let index =
429                    index_base + segment_offset * root_complex_per_segment + rc_index_in_segment;
430                let name = format!("s{}rc{}", segment, rc_index_in_segment);
431
432                let start_bus = rc_index_in_segment * bus_count_per_rc;
433                let end_bus = start_bus + bus_count_per_rc - 1;
434
435                let ports = (0..root_ports_per_root_complex)
436                    .map(|i| PciePortConfig {
437                        name: format!("s{}rc{}rp{}", segment, rc_index_in_segment, i),
438                        devfn: None,
439                        hotplug: true,
440                        acs_capabilities_supported: Some(0),
441                        cxl: false,
442                        pasid: false,
443                    })
444                    .collect();
445
446                self.config.pcie_root_complexes.push(PcieRootComplexConfig {
447                    index: index.try_into().unwrap(),
448                    name,
449                    segment: segment.try_into().unwrap(),
450                    start_bus: start_bus.try_into().unwrap(),
451                    end_bus: end_bus.try_into().unwrap(),
452                    low_mmio: PcieMmioRangeConfig::Dynamic {
453                        size: LOW_MMIO_SIZE,
454                    },
455                    high_mmio: PcieMmioRangeConfig::Dynamic {
456                        size: HIGH_MMIO_SIZE,
457                    },
458                    cxl: None,
459                    ports,
460                    iommu: None,
461                    vnode: None,
462                    preserve_bars: false,
463                });
464            }
465        }
466
467        self
468    }
469
470    /// Add a PCIe switch to the VM.
471    pub fn with_pcie_switch(
472        mut self,
473        port_name: &str,
474        switch_name: &str,
475        port_count: u8,
476        hotplug: bool,
477    ) -> Self {
478        self.config.pcie_switches.push(PcieSwitchConfig {
479            name: switch_name.to_string(),
480            parent_port: port_name.to_string(),
481            ports: (0..port_count)
482                .map(|i| PciePortConfig {
483                    name: format!("{switch_name}-downstream-{i}"),
484                    devfn: None,
485                    hotplug,
486                    acs_capabilities_supported: Some(0),
487                    cxl: false,
488                    pasid: false,
489                })
490                .collect(),
491        });
492        self
493    }
494
495    /// Enable SMMUv3 IOMMU on the specified root complexes (aarch64 only).
496    ///
497    /// Each name must match a root complex added via
498    /// [`with_pcie_root_topology`](Self::with_pcie_root_topology). The SMMU
499    /// provides stage 1 IOVA translation for devices behind those root
500    /// complexes.
501    pub fn with_smmu(mut self, rc_names: &[&str]) -> Self {
502        for name in rc_names {
503            self.pending_iommu.push((
504                name.to_string(),
505                PcieIommuConfig::Smmu {
506                    accel: false,
507                    oas: openvmm_defs::config::SmmuOas::Auto,
508                },
509            ));
510        }
511        self
512    }
513
514    /// Enable AMD IOMMU (AMD-Vi) on the specified root complexes.
515    ///
516    /// Each name must match a root complex added via
517    /// [`with_pcie_root_topology`](Self::with_pcie_root_topology). The IOMMU
518    /// appears at device 0 function 0 on each listed root complex; PCIe
519    /// devices behind those root complexes have DMA translated through
520    /// guest-programmed page tables and MSIs remapped through the interrupt
521    /// remapping table.
522    pub fn with_amd_iommu(mut self, rc_names: &[&str]) -> Self {
523        for name in rc_names {
524            self.pending_iommu
525                .push((name.to_string(), PcieIommuConfig::AmdVi));
526        }
527        self
528    }
529
530    /// Enable Intel VT-d IOMMU on the specified root complexes.
531    ///
532    /// Each name must match a root complex added via
533    /// [`with_pcie_root_topology`](Self::with_pcie_root_topology). The IOMMU
534    /// is a platform device discovered via the ACPI DMAR table; PCIe devices
535    /// behind those root complexes have DMA translated through
536    /// guest-programmed page tables and MSIs remapped through the interrupt
537    /// remapping table.
538    pub fn with_intel_vtd(mut self, rc_names: &[&str]) -> Self {
539        for name in rc_names {
540            self.pending_iommu
541                .push((name.to_string(), PcieIommuConfig::IntelVtd));
542        }
543        self
544    }
545
546    /// This is intended for special one-off use cases. As soon as something
547    /// is needed in multiple tests we should consider making it a supported
548    /// pattern.
549    pub fn with_custom_config(mut self, f: impl FnOnce(&mut Config)) -> Self {
550        f(&mut self.config);
551        self
552    }
553
554    /// Specifies whether VTL2 should be allowed to access VTL0 memory before it
555    /// sets any VTL protections.
556    ///
557    /// This is needed just for the TMK VMM, and only until it gains support for
558    /// setting VTL protections.
559    pub fn with_allow_early_vtl0_access(mut self, allow: bool) -> Self {
560        self.config
561            .hypervisor
562            .with_vtl2
563            .as_mut()
564            .unwrap()
565            .late_map_vtl0_memory =
566            (!allow).then_some(openvmm_defs::config::LateMapVtl0MemoryPolicy::InjectException);
567
568        self
569    }
570}