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    /// Skip automatic save/restore verification for devices that do not support it.
40    pub fn without_save_restore(mut self) -> Self {
41        self.skip_save_restore = true;
42        self
43    }
44
45    /// Enable the VTL0 alias map.
46    // TODO: Remove once #912 is fixed.
47    pub fn with_vtl0_alias_map(mut self) -> Self {
48        self.config
49            .hypervisor
50            .with_vtl2
51            .as_mut()
52            .expect("Not an openhcl config.")
53            .vtl0_alias_map = true;
54        self
55    }
56
57    /// Enable the battery for the VM.
58    pub fn with_battery(mut self) -> Self {
59        if self.resources.properties.is_openhcl {
60            self.ged.as_mut().unwrap().enable_battery = true;
61        } else {
62            self.config.chipset_devices.push(ChipsetDeviceHandle {
63                name: "battery".to_string(),
64                resource: BatteryDeviceHandleX64 {
65                    battery_status_recv: {
66                        let (tx, rx) = mesh::channel();
67                        tx.send(HostBatteryUpdate::default_present());
68                        rx
69                    },
70                }
71                .into_resource(),
72            });
73            if let LoadMode::Uefi { enable_battery, .. } = &mut self.config.load_mode {
74                *enable_battery = true;
75            }
76        }
77        self
78    }
79
80    /// Set test config for the GED's IGVM attest request handler
81    pub fn with_igvm_attest_test_config(mut self, config: IgvmAttestTestConfig) -> Self {
82        if !self.resources.properties.is_openhcl {
83            panic!("IGVM Attest test config is only supported for OpenHCL.")
84        };
85
86        let ged = self.ged.as_mut().expect("No GED to configure TPM");
87
88        ged.igvm_attest_test_config = Some(config);
89
90        self
91    }
92
93    /// Override the SMBIOS identity delivered to the guest, regardless of how
94    /// the VM is loaded.
95    ///
96    /// For OpenHCL the identity is forwarded to the paravisor over the Guest
97    /// Emulation Transport (GET), which synthesizes the guest's DMI tables from
98    /// it. For direct OpenVMM boot (Linux direct, UEFI, or PCAT) it is applied
99    /// to the loader's SMBIOS config. Each load path honors only the subset of
100    /// fields it can express and fails closed on the rest.
101    pub fn with_smbios(mut self, f: impl FnOnce(&mut smbios_defs::SmbiosConfig)) -> Self {
102        if self.resources.properties.is_openhcl {
103            let ged = self.ged.as_mut().expect("OpenHCL config must have a GED.");
104            f(&mut ged.smbios);
105        } else {
106            let smbios = match &mut self.config.load_mode {
107                LoadMode::Linux { smbios, .. }
108                | LoadMode::Uefi { smbios, .. }
109                | LoadMode::Pcat { smbios, .. } => &mut **smbios,
110                LoadMode::Igvm { .. } | LoadMode::None => {
111                    panic!("SMBIOS configuration is not supported for this load mode.")
112                }
113            };
114            f(smbios);
115        }
116        self
117    }
118
119    /// Enable a synthnic for the VM.
120    ///
121    /// Uses a mana emulator and the paravisor if a paravisor is present.
122    pub fn with_nic(mut self) -> Self {
123        let endpoint = net_backend_resources::consomme::ConsommeHandle {
124            cidr: None,
125            params: Default::default(),
126            ports: Vec::new(),
127            internal_dns: true,
128            recv: None,
129        }
130        .into_resource();
131        if let Some(vtl2_settings) = self.runtime_config.vtl2_settings.as_mut() {
132            self.config.vpci_devices.push(VpciDeviceConfig {
133                vtl: DeviceVtl::Vtl2,
134                instance_id: MANA_INSTANCE,
135                resource: GdmaDeviceHandle {
136                    vports: vec![VportDefinition {
137                        mac_address: NIC_MAC_ADDRESS,
138                        endpoint,
139                    }],
140                }
141                .into_resource(),
142                vnode: None,
143            });
144
145            vtl2_settings.dynamic.as_mut().unwrap().nic_devices.push(
146                vtl2_settings_proto::NicDeviceLegacy {
147                    instance_id: MANA_INSTANCE.to_string(),
148                    subordinate_instance_id: None,
149                    max_sub_channels: None,
150                },
151            );
152        } else {
153            const NETVSP_INSTANCE: Guid = guid::guid!("c6c46cc3-9302-4344-b206-aef65e5bd0a2");
154            self.config.vmbus_devices.push((
155                DeviceVtl::Vtl0,
156                netvsp_resources::NetvspHandle {
157                    instance_id: NETVSP_INSTANCE,
158                    mac_address: NIC_MAC_ADDRESS,
159                    endpoint,
160                    max_queues: None,
161                }
162                .into_resource(),
163            ));
164        }
165
166        self
167    }
168
169    /// Add a single-vport MANA VF to VTL2 and expose its synthetic NIC to VTL0.
170    pub fn with_mana_vf(self, instance_id: Guid, mac_address: MacAddress) -> Self {
171        self.with_mana_vf_vports(instance_id, [mac_address])
172    }
173
174    /// Add a MANA VF to VTL2 and expose one synthetic NIC per vport to VTL0.
175    pub fn with_mana_vf_vports(
176        mut self,
177        instance_id: Guid,
178        mac_addresses: impl IntoIterator<Item = MacAddress>,
179    ) -> Self {
180        let vtl2_settings = self
181            .runtime_config
182            .vtl2_settings
183            .as_mut()
184            .expect("a VTL2 MANA VF requires OpenHCL firmware");
185
186        let vports = mac_addresses
187            .into_iter()
188            .map(|mac_address| VportDefinition {
189                mac_address,
190                endpoint: net_backend_resources::consomme::ConsommeHandle {
191                    cidr: None,
192                    params: Default::default(),
193                    ports: Vec::new(),
194                    internal_dns: true,
195                    recv: None,
196                }
197                .into_resource(),
198            })
199            .collect::<Vec<_>>();
200
201        assert!(!vports.is_empty(), "a MANA VF requires at least one vport");
202
203        self.config.vpci_devices.push(VpciDeviceConfig {
204            vtl: DeviceVtl::Vtl2,
205            instance_id,
206            resource: GdmaDeviceHandle { vports }.into_resource(),
207            vnode: None,
208        });
209        vtl2_settings.dynamic.as_mut().unwrap().nic_devices.push(
210            vtl2_settings_proto::NicDeviceLegacy {
211                instance_id: instance_id.to_string(),
212                subordinate_instance_id: None,
213                max_sub_channels: None,
214            },
215        );
216
217        self
218    }
219
220    /// Add a PCIe NIC to the VM using the MANA emulator.
221    pub fn with_pcie_nic(mut self, port_name: &str, mac_address: MacAddress) -> Self {
222        let endpoint = net_backend_resources::consomme::ConsommeHandle {
223            cidr: None,
224            params: Default::default(),
225            ports: Vec::new(),
226            internal_dns: true,
227            recv: None,
228        }
229        .into_resource();
230        self.config.pcie_devices.push(PcieDeviceConfig {
231            port_name: port_name.to_string(),
232            resource: GdmaDeviceHandle {
233                vports: vec![VportDefinition {
234                    mac_address,
235                    endpoint,
236                }],
237            }
238            .into_resource(),
239        });
240
241        self
242    }
243
244    /// Add a PCIe NVMe device to the VM using the NVMe emulator.
245    pub fn with_pcie_nvme(mut self, port_name: &str, subsystem_id: Guid) -> Self {
246        self.config.pcie_devices.push(PcieDeviceConfig {
247            port_name: port_name.to_string(),
248            resource: NvmeControllerHandle {
249                subsystem_id,
250                max_io_queues: 64,
251                msix_count: 64,
252                namespaces: vec![NamespaceDefinition {
253                    nsid: 1,
254                    disk: LayeredDiskHandle::single_layer(RamDiskLayerHandle {
255                        len: Some(1024 * 1024),
256                        sector_size: None,
257                    })
258                    .into_resource(),
259                    read_only: false,
260                }],
261                requests: None,
262            }
263            .into_resource(),
264        });
265
266        self
267    }
268
269    /// Enable a virtio-net NIC for the VM backed by Consomme.
270    ///
271    /// This exposes a virtio-net device on a PCIe root port, suitable for
272    /// guests running virtio drivers (e.g. Linux with UEFI boot).
273    pub fn with_virtio_nic(self, port_name: &str, mac_address: MacAddress) -> Self {
274        self.with_virtio_nic_rx_gso(port_name, mac_address, false)
275    }
276
277    /// Enable a Consomme-backed virtio-net NIC with configurable receive GSO.
278    pub fn with_virtio_nic_rx_gso(
279        mut self,
280        port_name: &str,
281        mac_address: MacAddress,
282        enable_rx_gso: bool,
283    ) -> Self {
284        let endpoint = net_backend_resources::consomme::ConsommeHandle {
285            cidr: None,
286            params: Default::default(),
287            ports: Vec::new(),
288            internal_dns: true,
289            recv: None,
290        }
291        .into_resource();
292
293        self.config.pcie_devices.push(PcieDeviceConfig {
294            port_name: port_name.to_string(),
295            resource: virtio_resources::VirtioPciDeviceHandle(
296                virtio_resources::net::VirtioNetHandle {
297                    max_queues: None,
298                    enable_rx_gso,
299                    mac_address,
300                    endpoint,
301                }
302                .into_resource(),
303            )
304            .into_resource(),
305        });
306
307        self
308    }
309
310    /// Add a virtio-net NIC with consomme and TCP port forwarding for
311    /// pipette. Used for Windows no-vmbus guests where virtio-vsock is
312    /// unavailable.
313    ///
314    /// This configures consomme to forward the pipette TCP port from the
315    /// host into the guest, so the petri framework can connect to the
316    /// pipette agent over TCP.
317    pub fn with_tcp_pipette_nic(mut self, port_name: &str, mac_address: MacAddress) -> Self {
318        let (port_send, port_recv) = mesh::oneshot();
319        let endpoint = net_backend_resources::consomme::ConsommeHandle {
320            cidr: None,
321            params: Default::default(),
322            ports: vec![net_backend_resources::consomme::HostPortConfig {
323                protocol: net_backend_resources::consomme::HostPortProtocol::Tcp,
324                host_address: Some(net_backend_resources::consomme::HostIpAddress::Ipv4(
325                    std::net::Ipv4Addr::LOCALHOST,
326                )),
327                host_port: net_backend_resources::consomme::HostPort::Dynamic(port_send),
328                guest_port: pipette_client::PIPETTE_PORT as u16,
329            }],
330            internal_dns: true,
331            recv: None,
332        }
333        .into_resource();
334        self.config.pcie_devices.push(PcieDeviceConfig {
335            port_name: port_name.to_string(),
336            resource: virtio_resources::VirtioPciDeviceHandle(
337                virtio_resources::net::VirtioNetHandle {
338                    max_queues: None,
339                    enable_rx_gso: false,
340                    mac_address,
341                    endpoint,
342                }
343                .into_resource(),
344            )
345            .into_resource(),
346        });
347        self.resources.tcp_pipette_port = Some(port_recv);
348        self
349    }
350
351    /// Enable a synthnic for the VM backed by the Windows vmswitch
352    /// DirectIO (`-net dio`) backend.
353    ///
354    /// `switch_id`, when `None`, defaults to the Hyper-V Default Switch.
355    /// This requires the host to have Hyper-V installed and the chosen
356    /// switch available; tests that call this method should pre-resolve
357    /// a switch via [`super::find_switch`] (or an equivalent runtime
358    /// probe) and bail out with a clear error when the host does not
359    /// meet those requirements. The method itself panics if the switch
360    /// cannot be opened or a port cannot be created.
361    ///
362    /// The created vmswitch port handle is held in the petri (parent)
363    /// process for the lifetime of the VM. The kernel switch port object
364    /// is reference counted, so keeping the handle alive in this process
365    /// keeps the port usable from the child VMM process.
366    #[cfg(windows)]
367    pub fn with_dio_nic(mut self, switch_id: Option<Guid>) -> Self {
368        let switch_port_id = vmswitch::kernel::SwitchPortId {
369            switch: switch_id.unwrap_or(vmswitch::hcn::DEFAULT_SWITCH),
370            port: Guid::new_random(),
371        };
372        let _ = vmswitch::hcn::Network::open(&switch_port_id.switch)
373            .unwrap_or_else(|e| panic!("could not find switch {}: {e}", switch_port_id.switch));
374        let switch_port = vmswitch::kernel::SwitchPort::new(&switch_port_id)
375            .expect("failed to create vmswitch DIO port");
376        self.resources._switch_ports.push(switch_port);
377
378        let endpoint = net_backend_resources::dio::WindowsDirectIoHandle {
379            switch_port_id: net_backend_resources::dio::SwitchPortId {
380                switch: switch_port_id.switch,
381                port: switch_port_id.port,
382            },
383        }
384        .into_resource();
385
386        if let Some(vtl2_settings) = self.runtime_config.vtl2_settings.as_mut() {
387            self.config.vpci_devices.push(VpciDeviceConfig {
388                vtl: DeviceVtl::Vtl2,
389                instance_id: MANA_INSTANCE,
390                resource: GdmaDeviceHandle {
391                    vports: vec![VportDefinition {
392                        mac_address: NIC_MAC_ADDRESS,
393                        endpoint,
394                    }],
395                }
396                .into_resource(),
397                vnode: None,
398            });
399
400            vtl2_settings.dynamic.as_mut().unwrap().nic_devices.push(
401                vtl2_settings_proto::NicDeviceLegacy {
402                    instance_id: MANA_INSTANCE.to_string(),
403                    subordinate_instance_id: None,
404                    max_sub_channels: None,
405                },
406            );
407        } else {
408            const NETVSP_DIO_INSTANCE: Guid = guid::guid!("d1ff4c5a-1b3c-4f0d-8e10-1b9d8b1d1cee");
409            self.config.vmbus_devices.push((
410                DeviceVtl::Vtl0,
411                netvsp_resources::NetvspHandle {
412                    instance_id: NETVSP_DIO_INSTANCE,
413                    mac_address: NIC_MAC_ADDRESS,
414                    endpoint,
415                    max_queues: None,
416                }
417                .into_resource(),
418            ));
419        }
420
421        self
422    }
423
424    /// Load with the specified VTL2 relocation mode.
425    pub fn with_vtl2_relocation_mode(mut self, mode: Vtl2BaseAddressType) -> Self {
426        let LoadMode::Igvm {
427            vtl2_base_address, ..
428        } = &mut self.config.load_mode
429        else {
430            panic!("vtl2 relocation mode is only supported for OpenHCL firmware")
431        };
432        *vtl2_base_address = mode;
433        self
434    }
435
436    /// Use a file-backed memory region instead of anonymous RAM.
437    ///
438    /// The file at the given path will be created (or opened) and sized to
439    /// match the VM's configured memory. Guest memory is then backed by
440    /// this file, which persists across snapshot save/restore.
441    ///
442    /// This forces shared (non-private) memory, since a file-backed mapping
443    /// is incompatible with private anonymous RAM. Panics if the caller
444    /// explicitly requested private memory via
445    /// [`MemoryConfig::private_memory`](crate::MemoryConfig::private_memory),
446    /// rather than silently downgrading it.
447    pub fn with_memory_backing_file(mut self, path: impl Into<std::path::PathBuf>) -> Self {
448        assert_ne!(
449            self.requested_private_memory,
450            Some(true),
451            "with_memory_backing_file forces shared memory, which conflicts with \
452             the explicitly requested private memory"
453        );
454        self.memory_backing_file = Some(path.into());
455        for node in &mut self.config.numa.nodes {
456            if let Some(mem) = &mut node.mem {
457                mem.private_memory = false;
458            }
459        }
460        self
461    }
462
463    /// Use explicit hugetlb-backed guest memory.
464    ///
465    /// This forces shared (non-private) memory, since hugetlb backing
466    /// requires a file-backed mapping rather than private anonymous RAM.
467    /// Panics if the caller explicitly requested private memory via
468    /// [`MemoryConfig::private_memory`](crate::MemoryConfig::private_memory),
469    /// rather than silently downgrading it.
470    pub fn with_hugepages(mut self, hugepage_size: Option<u64>) -> Self {
471        assert_ne!(
472            self.requested_private_memory,
473            Some(true),
474            "with_hugepages forces shared memory, which conflicts with the \
475             explicitly requested private memory"
476        );
477        for node in &mut self.config.numa.nodes {
478            if let Some(mem) = &mut node.mem {
479                mem.hugepages = true;
480                mem.hugepage_size = hugepage_size;
481                mem.private_memory = false;
482            }
483        }
484        self
485    }
486
487    /// Add a symmetric PCIe topology to the VM based on some basic scale factors
488    ///
489    /// All root ports are named according to their index within their parent
490    /// using the naming scheme `sXrcYrpZ`. For example, the third root port on
491    /// the fourth root complex in segment 0 would be named `s0rc3rp2`.
492    ///
493    /// This may be called multiple times to build asymmetric topologies (e.g. a
494    /// different number of root complexes per segment). Each call appends its
495    /// root complexes to segments numbered after any added by previous calls,
496    /// so the segment numbers in the `sXrcY` names continue from where the last
497    /// call left off.
498    pub fn with_pcie_root_topology(
499        self,
500        segment_count: u64,
501        root_complex_per_segment: u64,
502        root_ports_per_root_complex: u64,
503    ) -> Self {
504        if segment_count == 0 {
505            return self;
506        }
507        // Offset the segments and global indices added by this call so that it
508        // can be called multiple times. New segments are numbered after any
509        // existing ones, and the global index continues from the existing
510        // root complex count.
511        let segment_base = self
512            .config
513            .pcie_root_complexes
514            .iter()
515            .map(|rc| u64::from(rc.segment) + 1)
516            .max()
517            .unwrap_or(0);
518        self.with_pcie_root_topology_at(
519            segment_base,
520            segment_count,
521            root_complex_per_segment,
522            root_ports_per_root_complex,
523        )
524    }
525
526    /// Append a topology starting at an explicit PCI segment, without
527    /// renumbering existing root complexes or their ports.
528    pub fn with_pcie_root_topology_at(
529        mut self,
530        segment_base: u64,
531        segment_count: u64,
532        root_complex_per_segment: u64,
533        root_ports_per_root_complex: u64,
534    ) -> Self {
535        let segment_end = segment_base
536            .checked_add(segment_count)
537            .filter(|end| *end <= u16::MAX as u64 + 1)
538            .expect("PCI segment range exceeds u16");
539        assert!(
540            !self
541                .config
542                .pcie_root_complexes
543                .iter()
544                .any(|rc| (segment_base..segment_end).contains(&u64::from(rc.segment))),
545            "PCI segment range overlaps an existing root complex"
546        );
547        assert!(
548            (1..=256).contains(&root_complex_per_segment),
549            "PCI topology requires between 1 and 256 root complexes per segment"
550        );
551        const LOW_MMIO_SIZE: u64 = 64 * 1024 * 1024;
552        const HIGH_MMIO_SIZE: u64 = 1024 * 1024 * 1024;
553        let index_base = self.config.pcie_root_complexes.len() as u64;
554
555        // Add the root complexes to the VM
556        for segment_offset in 0..segment_count {
557            let segment = segment_base + segment_offset;
558            let bus_count_per_rc = 256 / root_complex_per_segment;
559            for rc_index_in_segment in 0..root_complex_per_segment {
560                let index =
561                    index_base + segment_offset * root_complex_per_segment + rc_index_in_segment;
562                let name = format!("s{}rc{}", segment, rc_index_in_segment);
563
564                let start_bus = rc_index_in_segment * bus_count_per_rc;
565                let end_bus = start_bus + bus_count_per_rc - 1;
566
567                let ports = (0..root_ports_per_root_complex)
568                    .map(|i| PciePortConfig {
569                        name: format!("s{}rc{}rp{}", segment, rc_index_in_segment, i),
570                        devfn: None,
571                        hotplug: true,
572                        acs_capabilities_supported: Some(0),
573                        cxl: false,
574                        pasid: false,
575                    })
576                    .collect();
577
578                self.config.pcie_root_complexes.push(PcieRootComplexConfig {
579                    index: index.try_into().unwrap(),
580                    name,
581                    segment: segment.try_into().unwrap(),
582                    start_bus: start_bus.try_into().unwrap(),
583                    end_bus: end_bus.try_into().unwrap(),
584                    legacy_pci_config_io: false,
585                    low_mmio: PcieMmioRangeConfig::Dynamic {
586                        size: LOW_MMIO_SIZE,
587                    },
588                    high_mmio: PcieMmioRangeConfig::Dynamic {
589                        size: HIGH_MMIO_SIZE,
590                    },
591                    cxl: None,
592                    ports,
593                    iommu: None,
594                    vnode: None,
595                    preserve_bars: false,
596                });
597            }
598        }
599
600        self
601    }
602
603    pub(super) fn with_private_virtio_devices(mut self) -> Self {
604        if self.guest_shares.is_empty() && self.private_virtio_devices.is_empty() {
605            return self;
606        }
607        if self.config.pcie_root_complexes.is_empty() {
608            self = self.with_pcie_root_topology(1, 1, 0);
609        }
610        // Prefer an existing nonzero segment for guests that cannot use segment zero.
611        let root_index = self
612            .config
613            .pcie_root_complexes
614            .iter()
615            .position(|rc| rc.segment != 0)
616            .unwrap_or(0);
617        let mut port_names = self
618            .config
619            .pcie_root_complexes
620            .iter()
621            .flat_map(|rc| &rc.ports)
622            .chain(self.config.pcie_switches.iter().flat_map(|s| &s.ports))
623            .map(|p| p.name.clone())
624            .chain(self.config.pcie_devices.iter().map(|d| d.port_name.clone()))
625            .collect::<std::collections::HashSet<_>>();
626        let mut shares = std::mem::take(&mut self.guest_shares)
627            .into_iter()
628            .collect::<Vec<_>>();
629        shares.sort_by(|a, b| a.0.cmp(&b.0));
630        for (tag, share) in shares {
631            let root_path = share
632                .host_path
633                .into_os_string()
634                .into_string()
635                .expect("guest share path must be UTF-8");
636            self.private_virtio_devices.push((
637                "petri-share-",
638                virtio_resources::p9::VirtioPlan9Handle {
639                    tag,
640                    root_path,
641                    debug: false,
642                }
643                .into_resource(),
644            ));
645        }
646        for (prefix, resource) in std::mem::take(&mut self.private_virtio_devices) {
647            let port_name = (0..)
648                .map(|i| format!("{prefix}{i}"))
649                .find(|name| port_names.insert(name.clone()))
650                .unwrap();
651            self.config.pcie_root_complexes[root_index]
652                .ports
653                .push(PciePortConfig {
654                    name: port_name.clone(),
655                    devfn: None,
656                    hotplug: false,
657                    acs_capabilities_supported: Some(0),
658                    cxl: false,
659                    pasid: false,
660                });
661            self.config.pcie_devices.push(PcieDeviceConfig {
662                port_name,
663                resource: virtio_resources::VirtioPciDeviceHandle(resource).into_resource(),
664            });
665        }
666        self
667    }
668
669    /// Add a PCIe switch to the VM.
670    pub fn with_pcie_switch(
671        mut self,
672        port_name: &str,
673        switch_name: &str,
674        port_count: u8,
675        hotplug: bool,
676    ) -> Self {
677        self.config.pcie_switches.push(PcieSwitchConfig {
678            name: switch_name.to_string(),
679            parent_port: port_name.to_string(),
680            ports: (0..port_count)
681                .map(|i| PciePortConfig {
682                    name: format!("{switch_name}-downstream-{i}"),
683                    devfn: None,
684                    hotplug,
685                    acs_capabilities_supported: Some(0),
686                    cxl: false,
687                    pasid: false,
688                })
689                .collect(),
690        });
691        self
692    }
693
694    /// Enable SMMUv3 IOMMU on the specified root complexes (aarch64 only).
695    ///
696    /// Each name must match a root complex added via
697    /// [`with_pcie_root_topology`](Self::with_pcie_root_topology). The SMMU
698    /// provides stage 1 IOVA translation for devices behind those root
699    /// complexes.
700    pub fn with_smmu(mut self, rc_names: &[&str]) -> Self {
701        for name in rc_names {
702            self.pending_iommu.push((
703                name.to_string(),
704                PcieIommuConfig::Smmu {
705                    accel: false,
706                    oas: openvmm_defs::config::SmmuOas::Auto,
707                    ssidsize: openvmm_defs::config::SmmuSsidSize::Auto,
708                },
709            ));
710        }
711        self
712    }
713
714    /// Enable an accelerated (iommufd-nested) SMMUv3 on the specified root
715    /// complexes (aarch64 only).
716    ///
717    /// Like [`with_smmu`](Self::with_smmu), but the SMMU programs the host
718    /// IOMMU for hardware nested stage-1 translation, so VFIO devices behind
719    /// these root complexes are permitted (and their guest-programmed stage-1
720    /// tables are honored via a host nested HWPT). Requires a host SMMU that
721    /// supports iommufd nesting.
722    pub fn with_smmu_accel(mut self, rc_names: &[&str]) -> Self {
723        for name in rc_names {
724            self.pending_iommu.push((
725                name.to_string(),
726                PcieIommuConfig::Smmu {
727                    accel: true,
728                    oas: openvmm_defs::config::SmmuOas::Auto,
729                    ssidsize: openvmm_defs::config::SmmuSsidSize::Auto,
730                },
731            ));
732        }
733        self
734    }
735
736    /// Enable AMD IOMMU (AMD-Vi) on the specified root complexes.
737    ///
738    /// Each name must match a root complex added via
739    /// [`with_pcie_root_topology`](Self::with_pcie_root_topology). The IOMMU
740    /// appears at device 0 function 0 on each listed root complex; PCIe
741    /// devices behind those root complexes have DMA translated through
742    /// guest-programmed page tables and MSIs remapped through the interrupt
743    /// remapping table.
744    pub fn with_amd_iommu(mut self, rc_names: &[&str]) -> Self {
745        for name in rc_names {
746            self.pending_iommu
747                .push((name.to_string(), PcieIommuConfig::AmdVi));
748        }
749        self
750    }
751
752    /// Enable Intel VT-d IOMMU on the specified root complexes.
753    ///
754    /// Each name must match a root complex added via
755    /// [`with_pcie_root_topology`](Self::with_pcie_root_topology). The IOMMU
756    /// is a platform device discovered via the ACPI DMAR table; PCIe devices
757    /// behind those root complexes have DMA translated through
758    /// guest-programmed page tables and MSIs remapped through the interrupt
759    /// remapping table.
760    pub fn with_intel_vtd(mut self, rc_names: &[&str]) -> Self {
761        for name in rc_names {
762            self.pending_iommu
763                .push((name.to_string(), PcieIommuConfig::IntelVtd));
764        }
765        self
766    }
767
768    /// This is intended for special one-off use cases. As soon as something
769    /// is needed in multiple tests we should consider making it a supported
770    /// pattern.
771    pub fn with_custom_config(mut self, f: impl FnOnce(&mut Config)) -> Self {
772        f(&mut self.config);
773        self
774    }
775
776    /// Specifies whether VTL2 should be allowed to access VTL0 memory before it
777    /// sets any VTL protections.
778    ///
779    /// This is needed just for the TMK VMM, and only until it gains support for
780    /// setting VTL protections.
781    pub fn with_allow_early_vtl0_access(mut self, allow: bool) -> Self {
782        self.config
783            .hypervisor
784            .with_vtl2
785            .as_mut()
786            .unwrap()
787            .late_map_vtl0_memory =
788            (!allow).then_some(openvmm_defs::config::LateMapVtl0MemoryPolicy::InjectException);
789
790        self
791    }
792}