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 chipset_resources::acpi_power_button::PowerButtonRpc;
46use disk_backend_resources::DiskLayerDescription;
47use disk_backend_resources::LayeredDiskHandle;
48use disk_backend_resources::layer::DiskLayerHandle;
49use disk_backend_resources::layer::RamDiskLayerHandle;
50use disk_backend_resources::layer::SqliteAutoCacheDiskLayerHandle;
51use disk_backend_resources::layer::SqliteDiskLayerFormatParams;
52use disk_backend_resources::layer::SqliteDiskLayerHandle;
53use get_resources::ged::FirmwareEvent;
54use guid::Guid;
55use hyperv_ic_resources::shutdown::ShutdownRpc;
56use mesh::Receiver;
57use mesh::Sender;
58use net_backend_resources::mac_address::MacAddress;
59use openvmm_defs::config::Config;
60use openvmm_helpers::disk::OpenDiskOptions;
61use openvmm_helpers::disk::open_disk_type;
62use pal_async::DefaultDriver;
63use pal_async::socket::PolledSocket;
64use pal_async::task::Task;
65use petri_artifacts_common::tags::GuestQuirksInner;
66use petri_artifacts_common::tags::MachineArch;
67use petri_artifacts_core::ArtifactResolver;
68use petri_artifacts_core::ResolvedArtifact;
69use std::path::Path;
70use std::path::PathBuf;
71use std::sync::Arc;
72use std::time::Duration;
73use tempfile::TempPath;
74use unix_socket::UnixListener;
75use vm_resource::IntoResource;
76use vm_resource::Resource;
77use vm_resource::kind::DiskHandleKind;
78use vm_resource::kind::VirtioDeviceHandle;
79use vmgs_resources::VmgsDisk;
80use vmgs_resources::VmgsResource;
81
82/// The instance guid for the MANA nic automatically added when specifying [`PetriVmConfigOpenVmm::with_nic`]
83const MANA_INSTANCE: Guid = guid::guid!("f9641cf4-d915-4743-a7d8-efa75db7b85a");
84
85/// The MAC address used by the NIC assigned with [`PetriVmConfigOpenVmm::with_nic`].
86pub const NIC_MAC_ADDRESS: MacAddress = MacAddress::new([0x00, 0x15, 0x5D, 0x12, 0x12, 0x12]);
87
88/// OpenVMM Petri Backend
89#[derive(Debug)]
90pub struct OpenVmmPetriBackend {
91    openvmm_path: ResolvedArtifact,
92}
93
94impl crate::PetriVmBuilder<OpenVmmPetriBackend> {
95    /// Add a single-port Linux virtio console, captured in `hvc0.log`.
96    ///
97    /// Linux-direct boot selects `console=hvc0` and requires initrd-owned agent
98    /// startup. UEFI guests select their console through their boot configuration.
99    /// The firmware UART is unchanged.
100    pub fn with_virtio_console(mut self) -> Self {
101        assert!(
102            matches!(
103                self.config.firmware,
104                Firmware::LinuxDirect { .. } | Firmware::Uefi { .. }
105            ) && self.config.firmware.os_flavor() == petri_artifacts_common::tags::OsFlavor::Linux
106                && self.config.firmware.isolation().is_none(),
107            "virtio-console requires Linux-direct or non-isolated Linux UEFI"
108        );
109        self.use_virtio_console = true;
110        self
111    }
112}
113
114impl OpenVmmPetriBackend {
115    /// Launch a separately supplied, protocol-compatible native executable.
116    pub fn from_binary(path: PathBuf) -> Self {
117        Self {
118            openvmm_path: ResolvedArtifact::from_local_path(path),
119        }
120    }
121}
122
123#[async_trait]
124impl PetriVmmBackend for OpenVmmPetriBackend {
125    type VmmConfig = PetriVmConfigOpenVmm;
126    type VmRuntime = PetriVmOpenVmm;
127    const SUPPORTS_VMBUS: bool = true;
128    const SUPPORTS_FILE_SHARING: bool = cfg!(any(windows, target_os = "linux"));
129
130    fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool {
131        !(firmware.is_openhcl() && (!cfg!(windows) || arch == MachineArch::Aarch64))
132    }
133
134    fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks) {
135        (
136            firmware.quirks().openvmm,
137            VmmQuirks {
138                // Workaround for #3897
139                flaky_boot: firmware.is_pcat().then_some(Duration::from_secs(15)),
140            },
141        )
142    }
143
144    fn default_servicing_flags() -> OpenHclServicingFlags {
145        OpenHclServicingFlags {
146            enable_nvme_keepalive: true,
147            enable_mana_keepalive: true,
148            override_version_checks: false,
149            stop_timeout_hint_secs: None,
150        }
151    }
152
153    fn create_guest_dump_disk() -> anyhow::Result<
154        Option<(
155            Arc<TempPath>,
156            Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
157        )>,
158    > {
159        Ok(None) // TODO #2403
160    }
161
162    fn build_custom_init_script(_pipette_path: &str) -> Option<String> {
163        None
164    }
165
166    fn new(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
167        // OpenVMM guests must have the same arch as the host
168        assert_eq!(arch, MachineArch::host());
169        OpenVmmPetriBackend {
170            openvmm_path: resolver
171                .require(petri_artifacts_vmm_test::artifacts::OPENVMM_NATIVE)
172                .erase(),
173        }
174    }
175
176    async fn run(
177        self,
178        config: PetriVmConfig,
179        modify_vmm_config: Option<ModifyFn<Self::VmmConfig>>,
180        resources: &PetriVmResources,
181        properties: &PetriVmProperties,
182    ) -> anyhow::Result<(Self::VmRuntime, PetriVmRuntimeConfig)> {
183        let mut config =
184            PetriVmConfigOpenVmm::new(&self.openvmm_path, config, resources, properties).await?;
185
186        if let Some(f) = modify_vmm_config {
187            config = f.0(config);
188        }
189
190        config.run().await
191    }
192}
193
194/// Reusable OpenVMM configuration for tests that launch the same VM recipe in
195/// multiple worker processes.
196pub struct OpenVmmRelaunchConfig {
197    builder: crate::PetriVmBuilder<OpenVmmPetriBackend>,
198    /// Holds the disk write files shared by all launched workers.
199    _diff_dir: tempfile::TempDir,
200    /// Bounds the entire recipe, including time between worker launches.
201    _watchdog_task: Option<Task<()>>,
202}
203
204impl crate::PetriVmBuilder<OpenVmmPetriBackend> {
205    /// Prepare this configuration for launching multiple OpenVMM workers with
206    /// the same VM recipe.
207    ///
208    /// Differencing disks store their writes in files shared by all launched
209    /// workers, so a restored guest sees the disk writes made before the save.
210    pub fn prepare_openvmm_relaunch(self) -> anyhow::Result<OpenVmmRelaunchConfig> {
211        anyhow::ensure!(
212            self.modify_vmm_config.is_none(),
213            "OpenVMM relaunch configurations do not support modify_backend"
214        );
215
216        let mut builder = self.add_boot_disk()?.add_agent_disks()?;
217        if builder.uses_pipette_as_init() && builder.resources.prebuilt_initrd.is_none() {
218            builder.resources.prebuilt_initrd = Some(builder.prepare_initrd()?);
219        }
220
221        let diff_dir = tempfile::tempdir().context("failed to create disk write directory")?;
222        let config = &mut builder.config;
223        let drive_disks = config
224            .vmbus_storage_controllers
225            .values_mut()
226            .flat_map(|controller| controller.drives.values_mut())
227            .chain(config.pcie_nvme_drives.iter_mut().map(|d| &mut d.drive))
228            .chain(
229                config
230                    .pcie_virtio_blk_drives
231                    .iter_mut()
232                    .map(|d| &mut d.drive),
233            )
234            .chain(
235                config
236                    .firmware
237                    .ide_controllers_mut()
238                    .into_iter()
239                    .flatten()
240                    .flatten()
241                    .flatten(),
242            )
243            .filter_map(|drive| drive.disk.as_mut());
244        let vmgs_disk = match &mut config.vmgs {
245            PetriVmgsResource::Disk(vmgs)
246            | PetriVmgsResource::ReprovisionOnFailure(vmgs)
247            | PetriVmgsResource::ReprovisionSingle(vmgs) => Some(&mut vmgs.disk),
248            PetriVmgsResource::Ephemeral => None,
249        };
250        for (i, disk) in drive_disks.chain(vmgs_disk).enumerate() {
251            if let Disk::Differencing(base) = disk {
252                *disk = Disk::SqliteDifferencing {
253                    base: base.clone(),
254                    dbhd_path: diff_dir.path().join(format!("disk{i}.dbhd")),
255                };
256            }
257        }
258
259        let watchdog_task =
260            Self::start_watchdog_timer(&builder.resources.driver, builder.watchdog_timeout);
261        Ok(OpenVmmRelaunchConfig {
262            builder,
263            _diff_dir: diff_dir,
264            _watchdog_task: watchdog_task,
265        })
266    }
267}
268
269impl OpenVmmRelaunchConfig {
270    async fn config(
271        &self,
272        memory_backing_file: impl Into<PathBuf>,
273    ) -> anyhow::Result<PetriVmConfigOpenVmm> {
274        Ok(PetriVmConfigOpenVmm::new(
275            &self.builder.backend.openvmm_path,
276            self.builder.config.clone(),
277            &self.builder.resources,
278            &self.builder.properties(),
279        )
280        .await?
281        .with_memory_backing_file(memory_backing_file))
282    }
283
284    /// Launch a new worker using this VM recipe and file-backed guest memory.
285    pub async fn launch(
286        &self,
287        memory_backing_file: impl Into<PathBuf>,
288    ) -> anyhow::Result<PetriVmOpenVmm> {
289        let (vm, _) = self.config(memory_backing_file).await?.run_core().await?;
290        Ok(vm)
291    }
292
293    /// Launch a new worker using this VM recipe and restore its state from
294    /// serialized snapshot bytes.
295    pub async fn restore(
296        &self,
297        memory_backing_file: impl Into<PathBuf>,
298        saved_state: &[u8],
299    ) -> anyhow::Result<PetriVmOpenVmm> {
300        let (vm, _) = self
301            .config(memory_backing_file)
302            .await?
303            .run_restore(saved_state)
304            .await?;
305        Ok(vm)
306    }
307}
308
309/// Configuration state for a test VM.
310pub struct PetriVmConfigOpenVmm {
311    guest_shares: std::collections::HashMap<String, crate::GuestShare>,
312    private_virtio_devices: Vec<(&'static str, Resource<VirtioDeviceHandle>)>,
313    // Direct configuration related information.
314    runtime_config: PetriVmRuntimeConfig,
315    arch: MachineArch,
316    host_log_levels: Option<OpenvmmLogConfig>,
317    config: Config,
318    skip_save_restore: bool,
319
320    // Mesh host
321    mesh: mesh_process::Mesh,
322
323    // Runtime resources
324    resources: PetriVmResourcesOpenVmm,
325
326    // Logging
327    openvmm_log_file: PetriLogFile,
328
329    // File-backed guest memory.
330    memory_backing_file: Option<PathBuf>,
331
332    // The private-memory setting explicitly requested via
333    // `MemoryConfig::private_memory`, preserved so that backend methods which
334    // force shared memory (e.g. `with_hugepages`, `with_memory_backing_file`)
335    // can fail when the caller explicitly asked for private memory rather than
336    // silently downgrading it.
337    requested_private_memory: Option<bool>,
338
339    // Resources that are only used during startup.
340    ged: Option<get_resources::ged::GuestEmulationDeviceHandle>,
341    framebuffer_view: Option<framebuffer::View>,
342
343    // Deferred IOMMU configuration: (rc_name, iommu_config) pairs resolved
344    // against pcie_root_complexes at VM start time.
345    pending_iommu: Vec<(String, openvmm_defs::config::PcieIommuConfig)>,
346}
347/// Various channels and resources used to interact with the VM while it is running.
348struct PetriVmResourcesOpenVmm {
349    log_stream_tasks: Vec<Task<anyhow::Result<()>>>,
350    firmware_event_recv: Receiver<FirmwareEvent>,
351    ipmi_sel_event_recv: Receiver<get_resources::ged::IpmiSelEvent>,
352    shutdown_ic_send: Option<Sender<ShutdownRpc>>,
353    acpi_power_button_send: Option<Sender<PowerButtonRpc>>,
354    kvp_ic_send: Option<Sender<hyperv_ic_resources::kvp::KvpConnectRpc>>,
355    ged_send: Option<Sender<get_resources::ged::GuestEmulationRequest>>,
356    pipette_listener: PolledSocket<UnixListener>,
357    vtl2_pipette_listener: Option<PolledSocket<UnixListener>>,
358    linux_direct_serial_agent: Option<LinuxDirectSerialAgent>,
359
360    /// When set, the host connects to pipette via TCP through consomme
361    /// port forwarding instead of accepting on the Unix socket listener.
362    /// Used for Windows no-vmbus guests where virtio-vsock is unavailable.
363    /// The receiver yields the OS-assigned host port once the consomme
364    /// resolver has bound the socket.
365    tcp_pipette_port: Option<mesh::OneshotReceiver<u16>>,
366
367    // Externally injected management stuff also needed at runtime.
368    driver: DefaultDriver,
369    openvmm_path: ResolvedArtifact,
370    output_dir: PathBuf,
371
372    // TempPaths that cannot be dropped until the end.
373    vtl2_vsock_path: Option<TempPath>,
374    _vsock_path: TempPath,
375
376    // properties needed at runtime
377    properties: PetriVmProperties,
378
379    // vmswitch DirectIO switch port handles, held in the test (parent)
380    // process for the lifetime of the child VMM so the kernel port object
381    // survives until the VMM detaches.
382    #[cfg(windows)]
383    _switch_ports: Vec<vmswitch::kernel::SwitchPort>,
384}
385
386/// Discovers a usable Hyper-V virtual switch for `-net dio` tests.
387///
388/// Tries the well-known Default Switch GUID first (which is provisioned
389/// automatically when Hyper-V is installed). If that switch is not
390/// available (e.g. it was removed, or this host uses a different default
391/// switch SKU), falls back to enumerating all HCN networks and returning
392/// the first one reported.
393///
394/// Returns `None` when no switch can be opened — typically because
395/// Hyper-V is not installed, the user lacks privileges, or
396/// `computenetwork.dll` is missing.
397#[cfg(windows)]
398pub fn find_switch() -> Option<Guid> {
399    if vmswitch::hcn::Network::open(&vmswitch::hcn::DEFAULT_SWITCH).is_ok() {
400        return Some(vmswitch::hcn::DEFAULT_SWITCH);
401    }
402    let networks = match vmswitch::hcn::enumerate_networks() {
403        Ok(n) => n,
404        Err(e) => {
405            tracing::warn!(
406                error = &e as &dyn std::error::Error,
407                "failed to enumerate HCN networks"
408            );
409            return None;
410        }
411    };
412    networks.into_iter().find(|guid| {
413        if let Err(e) = vmswitch::hcn::Network::open(guid) {
414            tracing::debug!(
415                %guid,
416                error = &e as &dyn std::error::Error,
417                "skipping unopenable HCN network"
418            );
419            false
420        } else {
421            true
422        }
423    })
424}
425
426/// Discovers a usable Hyper-V virtual switch.
427///
428/// Always `None` on non-Windows platforms.
429#[cfg(not(windows))]
430pub fn find_switch() -> Option<Guid> {
431    None
432}
433
434fn ram_diff_layer() -> DiskLayerDescription {
435    RamDiskLayerHandle {
436        len: None,
437        sector_size: None,
438    }
439    .into_resource()
440    .into()
441}
442
443/// Returns a SQLite diff layer stored in `dbhd_path`, creating the file if it
444/// doesn't exist yet.
445fn sqlite_diff_layer(dbhd_path: &Path) -> DiskLayerDescription {
446    SqliteDiskLayerHandle {
447        dbhd_path: dbhd_path.display().to_string(),
448        format_dbhd: (!dbhd_path.exists()).then_some(SqliteDiskLayerFormatParams {
449            logically_read_only: false,
450            len: None,
451        }),
452    }
453    .into_resource()
454    .into()
455}
456
457fn layered_disk(
458    diff: DiskLayerDescription,
459    base: Vec<DiskLayerDescription>,
460) -> Resource<DiskHandleKind> {
461    LayeredDiskHandle {
462        layers: std::iter::once(diff).chain(base).collect(),
463    }
464    .into_resource()
465}
466
467/// Memory differencing disk over a local base disk.
468async fn memdiff_disk(path: &Path) -> anyhow::Result<Resource<DiskHandleKind>> {
469    Ok(layered_disk(
470        ram_diff_layer(),
471        local_base_layers(path).await?,
472    ))
473}
474
475/// Memory differencing disk over a remote base disk.
476fn memdiff_remote_disk(url: &str) -> anyhow::Result<Resource<DiskHandleKind>> {
477    Ok(layered_disk(ram_diff_layer(), remote_base_layers(url)))
478}
479
480/// SQLite differencing disk over a local base disk, storing writes in
481/// `dbhd_path`.
482async fn sqldiff_disk(path: &Path, dbhd_path: &Path) -> anyhow::Result<Resource<DiskHandleKind>> {
483    Ok(layered_disk(
484        sqlite_diff_layer(dbhd_path),
485        local_base_layers(path).await?,
486    ))
487}
488
489/// SQLite differencing disk over a remote base disk, storing writes in
490/// `dbhd_path`.
491fn sqldiff_remote_disk(url: &str, dbhd_path: &Path) -> anyhow::Result<Resource<DiskHandleKind>> {
492    Ok(layered_disk(
493        sqlite_diff_layer(dbhd_path),
494        remote_base_layers(url),
495    ))
496}
497
498async fn local_base_layers(path: &Path) -> anyhow::Result<Vec<DiskLayerDescription>> {
499    let disk = open_disk_type(
500        path,
501        OpenDiskOptions {
502            read_only: true,
503            direct: false,
504        },
505    )
506    .await
507    .with_context(|| format!("failed to open disk: {}", path.display()))?;
508    Ok(vec![DiskLayerHandle(disk).into_resource().into()])
509}
510
511fn remote_base_layers(url: &str) -> Vec<DiskLayerDescription> {
512    // Strip query parameters and fragments before checking the file extension.
513    let url_path = url.split(['?', '#']).next().unwrap_or(url);
514    let format = if url_path.ends_with(".vhd") || url_path.ends_with(".vmgs") {
515        disk_backend_resources::BlobDiskFormat::FixedVhd1
516    } else {
517        disk_backend_resources::BlobDiskFormat::Flat
518    };
519
520    let cache_dir = super::petri_disk_cache_dir();
521
522    // For VHD1-formatted blobs, let the auto-cache layer derive the cache key
523    // from the VHD's unique ID (a UUID embedded in the footer). This means the
524    // cache automatically invalidates when the image is replaced with a new one,
525    // even if the filename stays the same. For flat-format blobs (e.g. ISOs),
526    // fall back to the URL filename since there's no embedded ID.
527    let cache_key = match format {
528        disk_backend_resources::BlobDiskFormat::FixedVhd1 => None,
529        disk_backend_resources::BlobDiskFormat::Flat => {
530            Some(url_path.rsplit('/').next().unwrap_or(url_path).to_owned())
531        }
532    };
533
534    vec![
535        DiskLayerDescription {
536            read_cache: true,
537            write_through: false,
538            layer: SqliteAutoCacheDiskLayerHandle {
539                cache_path: cache_dir,
540                cache_key,
541            }
542            .into_resource(),
543        },
544        DiskLayerHandle(
545            disk_backend_resources::BlobDiskHandle {
546                url: url.to_owned(),
547                format,
548            }
549            .into_resource(),
550        )
551        .into_resource()
552        .into(),
553    ]
554}
555
556async fn memdiff_vmgs(vmgs: &PetriVmgsResource) -> anyhow::Result<VmgsResource> {
557    async fn convert_disk(disk: &PetriVmgsDisk) -> anyhow::Result<VmgsDisk> {
558        Ok(VmgsDisk {
559            disk: petri_disk_to_openvmm(&disk.disk).await?,
560            encryption_policy: disk.encryption_policy,
561        })
562    }
563
564    Ok(match vmgs {
565        PetriVmgsResource::Disk(disk) => VmgsResource::Disk(convert_disk(disk).await?),
566        PetriVmgsResource::ReprovisionOnFailure(disk) => {
567            VmgsResource::ReprovisionOnFailure(convert_disk(disk).await?)
568        }
569        PetriVmgsResource::ReprovisionSingle(disk) => {
570            VmgsResource::ReprovisionSingle(convert_disk(disk).await?)
571        }
572        PetriVmgsResource::Ephemeral => VmgsResource::Ephemeral,
573    })
574}
575
576async fn petri_disk_to_openvmm(disk: &Disk) -> anyhow::Result<Resource<DiskHandleKind>> {
577    Ok(match disk {
578        Disk::Memory(len) => LayeredDiskHandle::single_layer(RamDiskLayerHandle {
579            len: Some(*len),
580            sector_size: None,
581        })
582        .into_resource(),
583        Disk::Differencing(DiskPath::Local(path)) => memdiff_disk(path).await?,
584        Disk::Differencing(DiskPath::Remote { url }) => memdiff_remote_disk(url)?,
585        Disk::SqliteDifferencing {
586            base: DiskPath::Local(path),
587            dbhd_path,
588        } => sqldiff_disk(path, dbhd_path).await?,
589        Disk::SqliteDifferencing {
590            base: DiskPath::Remote { url },
591            dbhd_path,
592        } => sqldiff_remote_disk(url, dbhd_path)?,
593        Disk::Persistent(path) => {
594            open_disk_type(
595                path.as_ref(),
596                OpenDiskOptions {
597                    read_only: false,
598                    direct: false,
599                },
600            )
601            .await?
602        }
603        Disk::Temporary(path) => {
604            open_disk_type(
605                path.as_ref(),
606                OpenDiskOptions {
607                    read_only: false,
608                    direct: false,
609                },
610            )
611            .await?
612        }
613    })
614}