Skip to main content

petri/vm/openvmm/
mod.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Code managing the lifetime of a `PetriVmOpenVmm`. All VMs live the same lifecycle:
5//! * A `PetriVmConfigOpenVmm` is built for the given firmware and architecture in `construct`.
6//! * The configuration is optionally modified from the defaults using the helpers in `modify`.
7//! * The `PetriVmOpenVmm` is started by the code in `start`.
8//! * The VM is interacted with through the methods in `runtime`.
9//! * The VM is either shut down by the code in `runtime`, or gets dropped and cleaned up automatically.
10
11mod construct;
12#[cfg(target_os = "linux")]
13mod hugetlb;
14mod modify;
15mod runtime;
16mod start;
17
18#[cfg(target_os = "linux")]
19pub use hugetlb::HUGETLB_2MB_PAGE_SIZE;
20#[cfg(target_os = "linux")]
21pub use hugetlb::ensure_2mb_hugetlb_pages;
22pub use runtime::OpenVmmFramebufferAccess;
23pub use runtime::OpenVmmInspector;
24pub use runtime::PetriVmOpenVmm;
25
26use crate::Disk;
27use crate::DiskPath;
28use crate::Firmware;
29use crate::ModifyFn;
30use crate::OpenHclServicingFlags;
31use crate::OpenvmmLogConfig;
32use crate::PetriLogFile;
33use crate::PetriVmConfig;
34use crate::PetriVmResources;
35use crate::PetriVmRuntimeConfig;
36use crate::PetriVmgsDisk;
37use crate::PetriVmgsResource;
38use crate::PetriVmmBackend;
39use crate::VmmQuirks;
40use crate::linux_direct_serial_agent::LinuxDirectSerialAgent;
41use crate::vm::PetriVmProperties;
42use anyhow::Context;
43use async_trait::async_trait;
44use disk_backend_resources::DiskLayerDescription;
45use disk_backend_resources::LayeredDiskHandle;
46use disk_backend_resources::layer::DiskLayerHandle;
47use disk_backend_resources::layer::RamDiskLayerHandle;
48use disk_backend_resources::layer::SqliteAutoCacheDiskLayerHandle;
49use get_resources::ged::FirmwareEvent;
50use guid::Guid;
51use hyperv_ic_resources::shutdown::ShutdownRpc;
52use mesh::Receiver;
53use mesh::Sender;
54use net_backend_resources::mac_address::MacAddress;
55use openvmm_defs::config::Config;
56use openvmm_helpers::disk::OpenDiskOptions;
57use openvmm_helpers::disk::open_disk_type;
58use pal_async::DefaultDriver;
59use pal_async::socket::PolledSocket;
60use pal_async::task::Task;
61use petri_artifacts_common::tags::GuestQuirksInner;
62use petri_artifacts_common::tags::MachineArch;
63use petri_artifacts_core::ArtifactResolver;
64use petri_artifacts_core::ResolvedArtifact;
65use std::path::Path;
66use std::path::PathBuf;
67use std::sync::Arc;
68use std::time::Duration;
69use tempfile::TempPath;
70use unix_socket::UnixListener;
71use vm_resource::IntoResource;
72use vm_resource::Resource;
73use vm_resource::kind::DiskHandleKind;
74use vmgs_resources::VmgsDisk;
75use vmgs_resources::VmgsResource;
76
77/// The instance guid for the MANA nic automatically added when specifying [`PetriVmConfigOpenVmm::with_nic`]
78const MANA_INSTANCE: Guid = guid::guid!("f9641cf4-d915-4743-a7d8-efa75db7b85a");
79
80/// The MAC address used by the NIC assigned with [`PetriVmConfigOpenVmm::with_nic`].
81pub const NIC_MAC_ADDRESS: MacAddress = MacAddress::new([0x00, 0x15, 0x5D, 0x12, 0x12, 0x12]);
82
83/// OpenVMM Petri Backend
84#[derive(Debug)]
85pub struct OpenVmmPetriBackend {
86    openvmm_path: ResolvedArtifact,
87}
88
89#[async_trait]
90impl PetriVmmBackend for OpenVmmPetriBackend {
91    type VmmConfig = PetriVmConfigOpenVmm;
92    type VmRuntime = PetriVmOpenVmm;
93
94    fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool {
95        arch == MachineArch::host()
96            && !(firmware.is_openhcl() && (!cfg!(windows) || arch == MachineArch::Aarch64))
97            && !(firmware.is_pcat() && arch == MachineArch::Aarch64)
98    }
99
100    fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks) {
101        (
102            firmware.quirks().openvmm,
103            VmmQuirks {
104                // Workaround for #3897
105                flaky_boot: firmware.is_pcat().then_some(Duration::from_secs(15)),
106            },
107        )
108    }
109
110    fn default_servicing_flags() -> OpenHclServicingFlags {
111        OpenHclServicingFlags {
112            enable_nvme_keepalive: true,
113            enable_mana_keepalive: true,
114            override_version_checks: false,
115            stop_timeout_hint_secs: None,
116        }
117    }
118
119    fn create_guest_dump_disk() -> anyhow::Result<
120        Option<(
121            Arc<TempPath>,
122            Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
123        )>,
124    > {
125        Ok(None) // TODO #2403
126    }
127
128    fn new(resolver: &ArtifactResolver<'_>) -> Self {
129        OpenVmmPetriBackend {
130            openvmm_path: resolver
131                .require(petri_artifacts_vmm_test::artifacts::OPENVMM_NATIVE)
132                .erase(),
133        }
134    }
135
136    async fn run(
137        self,
138        config: PetriVmConfig,
139        modify_vmm_config: Option<ModifyFn<Self::VmmConfig>>,
140        resources: &PetriVmResources,
141        properties: PetriVmProperties,
142    ) -> anyhow::Result<(Self::VmRuntime, PetriVmRuntimeConfig)> {
143        let mut config =
144            PetriVmConfigOpenVmm::new(&self.openvmm_path, config, resources, properties).await?;
145
146        if let Some(f) = modify_vmm_config {
147            config = f.0(config);
148        }
149
150        config.run().await
151    }
152}
153
154/// Configuration state for a test VM.
155pub struct PetriVmConfigOpenVmm {
156    // Direct configuration related information.
157    runtime_config: PetriVmRuntimeConfig,
158    arch: MachineArch,
159    host_log_levels: Option<OpenvmmLogConfig>,
160    config: Config,
161
162    // Mesh host
163    mesh: mesh_process::Mesh,
164
165    // Runtime resources
166    resources: PetriVmResourcesOpenVmm,
167
168    // Logging
169    openvmm_log_file: PetriLogFile,
170
171    // File-backed guest memory.
172    memory_backing_file: Option<PathBuf>,
173
174    // The private-memory setting explicitly requested via
175    // `MemoryConfig::private_memory`, preserved so that backend methods which
176    // force shared memory (e.g. `with_hugepages`, `with_memory_backing_file`)
177    // can fail when the caller explicitly asked for private memory rather than
178    // silently downgrading it.
179    requested_private_memory: Option<bool>,
180
181    // Resources that are only used during startup.
182    ged: Option<get_resources::ged::GuestEmulationDeviceHandle>,
183    framebuffer_view: Option<framebuffer::View>,
184
185    // Deferred IOMMU configuration: (rc_name, iommu_config) pairs resolved
186    // against pcie_root_complexes at VM start time.
187    pending_iommu: Vec<(String, openvmm_defs::config::PcieIommuConfig)>,
188}
189/// Various channels and resources used to interact with the VM while it is running.
190struct PetriVmResourcesOpenVmm {
191    log_stream_tasks: Vec<Task<anyhow::Result<()>>>,
192    firmware_event_recv: Receiver<FirmwareEvent>,
193    ipmi_sel_event_recv: Receiver<get_resources::ged::IpmiSelEvent>,
194    shutdown_ic_send: Option<Sender<ShutdownRpc>>,
195    kvp_ic_send: Option<Sender<hyperv_ic_resources::kvp::KvpConnectRpc>>,
196    ged_send: Option<Sender<get_resources::ged::GuestEmulationRequest>>,
197    pipette_listener: PolledSocket<UnixListener>,
198    vtl2_pipette_listener: Option<PolledSocket<UnixListener>>,
199    linux_direct_serial_agent: Option<LinuxDirectSerialAgent>,
200
201    /// When set, the host connects to pipette via TCP through consomme
202    /// port forwarding instead of accepting on the Unix socket listener.
203    /// Used for Windows no-vmbus guests where virtio-vsock is unavailable.
204    /// The receiver yields the OS-assigned host port once the consomme
205    /// resolver has bound the socket.
206    tcp_pipette_port: Option<mesh::OneshotReceiver<u16>>,
207
208    // Externally injected management stuff also needed at runtime.
209    driver: DefaultDriver,
210    openvmm_path: ResolvedArtifact,
211    output_dir: PathBuf,
212
213    // TempPaths that cannot be dropped until the end.
214    vtl2_vsock_path: Option<TempPath>,
215    _vsock_path: TempPath,
216
217    // properties needed at runtime
218    properties: PetriVmProperties,
219
220    // vmswitch DirectIO switch port handles, held in the test (parent)
221    // process for the lifetime of the child VMM so the kernel port object
222    // survives until the VMM detaches.
223    #[cfg(windows)]
224    _switch_ports: Vec<vmswitch::kernel::SwitchPort>,
225}
226
227/// Discovers a usable Hyper-V virtual switch for `-net dio` tests.
228///
229/// Tries the well-known Default Switch GUID first (which is provisioned
230/// automatically when Hyper-V is installed). If that switch is not
231/// available (e.g. it was removed, or this host uses a different default
232/// switch SKU), falls back to enumerating all HCN networks and returning
233/// the first one reported.
234///
235/// Returns `None` when no switch can be opened — typically because
236/// Hyper-V is not installed, the user lacks privileges, or
237/// `computenetwork.dll` is missing.
238#[cfg(windows)]
239pub fn find_switch() -> Option<Guid> {
240    if vmswitch::hcn::Network::open(&vmswitch::hcn::DEFAULT_SWITCH).is_ok() {
241        return Some(vmswitch::hcn::DEFAULT_SWITCH);
242    }
243    let networks = match vmswitch::hcn::enumerate_networks() {
244        Ok(n) => n,
245        Err(e) => {
246            tracing::warn!(
247                error = &e as &dyn std::error::Error,
248                "failed to enumerate HCN networks"
249            );
250            return None;
251        }
252    };
253    networks.into_iter().find(|guid| {
254        if let Err(e) = vmswitch::hcn::Network::open(guid) {
255            tracing::debug!(
256                %guid,
257                error = &e as &dyn std::error::Error,
258                "skipping unopenable HCN network"
259            );
260            false
261        } else {
262            true
263        }
264    })
265}
266
267/// Discovers a usable Hyper-V virtual switch.
268///
269/// Always `None` on non-Windows platforms.
270#[cfg(not(windows))]
271pub fn find_switch() -> Option<Guid> {
272    None
273}
274
275async fn memdiff_disk(path: &Path) -> anyhow::Result<Resource<DiskHandleKind>> {
276    let disk = open_disk_type(
277        path,
278        OpenDiskOptions {
279            read_only: true,
280            direct: false,
281        },
282    )
283    .await
284    .with_context(|| format!("failed to open disk: {}", path.display()))?;
285    Ok(LayeredDiskHandle {
286        layers: vec![
287            RamDiskLayerHandle {
288                len: None,
289                sector_size: None,
290            }
291            .into_resource()
292            .into(),
293            DiskLayerHandle(disk).into_resource().into(),
294        ],
295    }
296    .into_resource())
297}
298
299fn memdiff_remote_disk(url: &str) -> anyhow::Result<Resource<DiskHandleKind>> {
300    // Strip query parameters and fragments before checking the file extension.
301    let url_path = url.split(['?', '#']).next().unwrap_or(url);
302    let format = if url_path.ends_with(".vhd") || url_path.ends_with(".vmgs") {
303        disk_backend_resources::BlobDiskFormat::FixedVhd1
304    } else {
305        disk_backend_resources::BlobDiskFormat::Flat
306    };
307
308    let cache_dir = super::petri_disk_cache_dir();
309
310    // For VHD1-formatted blobs, let the auto-cache layer derive the cache key
311    // from the VHD's unique ID (a UUID embedded in the footer). This means the
312    // cache automatically invalidates when the image is replaced with a new one,
313    // even if the filename stays the same. For flat-format blobs (e.g. ISOs),
314    // fall back to the URL filename since there's no embedded ID.
315    let cache_key = match format {
316        disk_backend_resources::BlobDiskFormat::FixedVhd1 => None,
317        disk_backend_resources::BlobDiskFormat::Flat => {
318            Some(url_path.rsplit('/').next().unwrap_or(url_path).to_owned())
319        }
320    };
321
322    Ok(LayeredDiskHandle {
323        layers: vec![
324            RamDiskLayerHandle {
325                len: None,
326                sector_size: None,
327            }
328            .into_resource()
329            .into(),
330            DiskLayerDescription {
331                read_cache: true,
332                write_through: false,
333                layer: SqliteAutoCacheDiskLayerHandle {
334                    cache_path: cache_dir,
335                    cache_key,
336                }
337                .into_resource(),
338            },
339            DiskLayerHandle(
340                disk_backend_resources::BlobDiskHandle {
341                    url: url.to_owned(),
342                    format,
343                }
344                .into_resource(),
345            )
346            .into_resource()
347            .into(),
348        ],
349    }
350    .into_resource())
351}
352
353async fn memdiff_vmgs(vmgs: &PetriVmgsResource) -> anyhow::Result<VmgsResource> {
354    async fn convert_disk(disk: &PetriVmgsDisk) -> anyhow::Result<VmgsDisk> {
355        Ok(VmgsDisk {
356            disk: petri_disk_to_openvmm(&disk.disk).await?,
357            encryption_policy: disk.encryption_policy,
358        })
359    }
360
361    Ok(match vmgs {
362        PetriVmgsResource::Disk(disk) => VmgsResource::Disk(convert_disk(disk).await?),
363        PetriVmgsResource::ReprovisionOnFailure(disk) => {
364            VmgsResource::ReprovisionOnFailure(convert_disk(disk).await?)
365        }
366        PetriVmgsResource::Reprovision(disk) => {
367            VmgsResource::Reprovision(convert_disk(disk).await?)
368        }
369        PetriVmgsResource::Ephemeral => VmgsResource::Ephemeral,
370    })
371}
372
373async fn petri_disk_to_openvmm(disk: &Disk) -> anyhow::Result<Resource<DiskHandleKind>> {
374    Ok(match disk {
375        Disk::Memory(len) => LayeredDiskHandle::single_layer(RamDiskLayerHandle {
376            len: Some(*len),
377            sector_size: None,
378        })
379        .into_resource(),
380        Disk::Differencing(DiskPath::Local(path)) => memdiff_disk(path).await?,
381        Disk::Differencing(DiskPath::Remote { url }) => memdiff_remote_disk(url)?,
382        Disk::Persistent(path) => {
383            open_disk_type(
384                path.as_ref(),
385                OpenDiskOptions {
386                    read_only: false,
387                    direct: false,
388                },
389            )
390            .await?
391        }
392        Disk::Temporary(path) => {
393            open_disk_type(
394                path.as_ref(),
395                OpenDiskOptions {
396                    read_only: false,
397                    direct: false,
398                },
399            )
400            .await?
401        }
402    })
403}