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