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;
12mod modify;
13mod runtime;
14mod start;
15
16pub use runtime::OpenVmmFramebufferAccess;
17pub use runtime::OpenVmmInspector;
18pub use runtime::PetriVmOpenVmm;
19
20use crate::Disk;
21use crate::DiskPath;
22use crate::Firmware;
23use crate::ModifyFn;
24use crate::OpenHclServicingFlags;
25use crate::OpenvmmLogConfig;
26use crate::PetriLogFile;
27use crate::PetriVmConfig;
28use crate::PetriVmResources;
29use crate::PetriVmRuntimeConfig;
30use crate::PetriVmgsDisk;
31use crate::PetriVmgsResource;
32use crate::PetriVmmBackend;
33use crate::VmmQuirks;
34use crate::linux_direct_serial_agent::LinuxDirectSerialAgent;
35use crate::vm::PetriVmProperties;
36use anyhow::Context;
37use async_trait::async_trait;
38use disk_backend_resources::DiskLayerDescription;
39use disk_backend_resources::LayeredDiskHandle;
40use disk_backend_resources::layer::DiskLayerHandle;
41use disk_backend_resources::layer::RamDiskLayerHandle;
42use disk_backend_resources::layer::SqliteAutoCacheDiskLayerHandle;
43use get_resources::ged::FirmwareEvent;
44use guid::Guid;
45use hyperv_ic_resources::shutdown::ShutdownRpc;
46use mesh::Receiver;
47use mesh::Sender;
48use net_backend_resources::mac_address::MacAddress;
49use openvmm_defs::config::Config;
50use openvmm_helpers::disk::OpenDiskOptions;
51use openvmm_helpers::disk::open_disk_type;
52use pal_async::DefaultDriver;
53use pal_async::socket::PolledSocket;
54use pal_async::task::Task;
55use petri_artifacts_common::tags::GuestQuirksInner;
56use petri_artifacts_common::tags::MachineArch;
57use petri_artifacts_core::ArtifactResolver;
58use petri_artifacts_core::ResolvedArtifact;
59use std::path::Path;
60use std::path::PathBuf;
61use std::sync::Arc;
62use std::time::Duration;
63use tempfile::TempPath;
64use unix_socket::UnixListener;
65use vm_resource::IntoResource;
66use vm_resource::Resource;
67use vm_resource::kind::DiskHandleKind;
68use vmgs_resources::VmgsDisk;
69use vmgs_resources::VmgsResource;
70
71/// The instance guid for the MANA nic automatically added when specifying `PetriVmConfigOpenVmm::with_nic`
72const MANA_INSTANCE: Guid = guid::guid!("f9641cf4-d915-4743-a7d8-efa75db7b85a");
73
74/// The MAC address used by the NIC assigned with [`PetriVmConfigOpenVmm::with_nic`].
75pub const NIC_MAC_ADDRESS: MacAddress = MacAddress::new([0x00, 0x15, 0x5D, 0x12, 0x12, 0x12]);
76
77/// OpenVMM Petri Backend
78#[derive(Debug)]
79pub struct OpenVmmPetriBackend {
80    openvmm_path: ResolvedArtifact,
81}
82
83#[async_trait]
84impl PetriVmmBackend for OpenVmmPetriBackend {
85    type VmmConfig = PetriVmConfigOpenVmm;
86    type VmRuntime = PetriVmOpenVmm;
87
88    fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool {
89        arch == MachineArch::host()
90            && !(firmware.is_openhcl() && (!cfg!(windows) || arch == MachineArch::Aarch64))
91            && !(firmware.is_pcat() && arch == MachineArch::Aarch64)
92    }
93
94    fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks) {
95        (
96            firmware.quirks().openvmm,
97            VmmQuirks {
98                // Workaround for #1684
99                flaky_boot: firmware.is_pcat().then_some(Duration::from_secs(15)),
100            },
101        )
102    }
103
104    fn default_servicing_flags() -> OpenHclServicingFlags {
105        OpenHclServicingFlags {
106            enable_nvme_keepalive: true,
107            enable_mana_keepalive: true,
108            override_version_checks: false,
109            stop_timeout_hint_secs: None,
110        }
111    }
112
113    fn create_guest_dump_disk() -> anyhow::Result<
114        Option<(
115            Arc<TempPath>,
116            Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
117        )>,
118    > {
119        Ok(None) // TODO #2403
120    }
121
122    fn new(resolver: &ArtifactResolver<'_>) -> Self {
123        OpenVmmPetriBackend {
124            openvmm_path: resolver
125                .require(petri_artifacts_vmm_test::artifacts::OPENVMM_NATIVE)
126                .erase(),
127        }
128    }
129
130    async fn run(
131        self,
132        config: PetriVmConfig,
133        modify_vmm_config: Option<ModifyFn<Self::VmmConfig>>,
134        resources: &PetriVmResources,
135        properties: PetriVmProperties,
136    ) -> anyhow::Result<(Self::VmRuntime, PetriVmRuntimeConfig)> {
137        let mut config =
138            PetriVmConfigOpenVmm::new(&self.openvmm_path, config, resources, properties).await?;
139
140        if let Some(f) = modify_vmm_config {
141            config = f.0(config);
142        }
143
144        config.run().await
145    }
146}
147
148/// Configuration state for a test VM.
149pub struct PetriVmConfigOpenVmm {
150    // Direct configuration related information.
151    runtime_config: PetriVmRuntimeConfig,
152    arch: MachineArch,
153    host_log_levels: Option<OpenvmmLogConfig>,
154    config: Config,
155
156    // Mesh host
157    mesh: mesh_process::Mesh,
158
159    // Runtime resources
160    resources: PetriVmResourcesOpenVmm,
161
162    // Logging
163    openvmm_log_file: PetriLogFile,
164
165    // File-backed guest memory.
166    memory_backing_file: Option<PathBuf>,
167
168    // Resources that are only used during startup.
169    ged: Option<get_resources::ged::GuestEmulationDeviceHandle>,
170    framebuffer_view: Option<framebuffer::View>,
171}
172/// Various channels and resources used to interact with the VM while it is running.
173struct PetriVmResourcesOpenVmm {
174    log_stream_tasks: Vec<Task<anyhow::Result<()>>>,
175    firmware_event_recv: Receiver<FirmwareEvent>,
176    shutdown_ic_send: Sender<ShutdownRpc>,
177    kvp_ic_send: Sender<hyperv_ic_resources::kvp::KvpConnectRpc>,
178    ged_send: Option<Sender<get_resources::ged::GuestEmulationRequest>>,
179    pipette_listener: PolledSocket<UnixListener>,
180    vtl2_pipette_listener: Option<PolledSocket<UnixListener>>,
181    linux_direct_serial_agent: Option<LinuxDirectSerialAgent>,
182
183    // Externally injected management stuff also needed at runtime.
184    driver: DefaultDriver,
185    openvmm_path: ResolvedArtifact,
186    output_dir: PathBuf,
187
188    // TempPaths that cannot be dropped until the end.
189    vtl2_vsock_path: Option<TempPath>,
190    _vmbus_vsock_path: TempPath,
191
192    // properties needed at runtime
193    properties: PetriVmProperties,
194}
195
196async fn memdiff_disk(path: &Path) -> anyhow::Result<Resource<DiskHandleKind>> {
197    let disk = open_disk_type(
198        path,
199        OpenDiskOptions {
200            read_only: true,
201            direct: false,
202        },
203    )
204    .await
205    .with_context(|| format!("failed to open disk: {}", path.display()))?;
206    Ok(LayeredDiskHandle {
207        layers: vec![
208            RamDiskLayerHandle {
209                len: None,
210                sector_size: None,
211            }
212            .into_resource()
213            .into(),
214            DiskLayerHandle(disk).into_resource().into(),
215        ],
216    }
217    .into_resource())
218}
219
220fn memdiff_remote_disk(url: &str) -> anyhow::Result<Resource<DiskHandleKind>> {
221    // Strip query parameters and fragments before checking the file extension.
222    let url_path = url.split(['?', '#']).next().unwrap_or(url);
223    let format = if url_path.ends_with(".vhd") || url_path.ends_with(".vmgs") {
224        disk_backend_resources::BlobDiskFormat::FixedVhd1
225    } else {
226        disk_backend_resources::BlobDiskFormat::Flat
227    };
228
229    let cache_dir = super::petri_disk_cache_dir();
230
231    // For VHD1-formatted blobs, let the auto-cache layer derive the cache key
232    // from the VHD's unique ID (a UUID embedded in the footer). This means the
233    // cache automatically invalidates when the image is replaced with a new one,
234    // even if the filename stays the same. For flat-format blobs (e.g. ISOs),
235    // fall back to the URL filename since there's no embedded ID.
236    let cache_key = match format {
237        disk_backend_resources::BlobDiskFormat::FixedVhd1 => None,
238        disk_backend_resources::BlobDiskFormat::Flat => {
239            Some(url_path.rsplit('/').next().unwrap_or(url_path).to_owned())
240        }
241    };
242
243    Ok(LayeredDiskHandle {
244        layers: vec![
245            RamDiskLayerHandle {
246                len: None,
247                sector_size: None,
248            }
249            .into_resource()
250            .into(),
251            DiskLayerDescription {
252                read_cache: true,
253                write_through: false,
254                layer: SqliteAutoCacheDiskLayerHandle {
255                    cache_path: cache_dir,
256                    cache_key,
257                }
258                .into_resource(),
259            },
260            DiskLayerHandle(
261                disk_backend_resources::BlobDiskHandle {
262                    url: url.to_owned(),
263                    format,
264                }
265                .into_resource(),
266            )
267            .into_resource()
268            .into(),
269        ],
270    }
271    .into_resource())
272}
273
274async fn memdiff_vmgs(vmgs: &PetriVmgsResource) -> anyhow::Result<VmgsResource> {
275    async fn convert_disk(disk: &PetriVmgsDisk) -> anyhow::Result<VmgsDisk> {
276        Ok(VmgsDisk {
277            disk: petri_disk_to_openvmm(&disk.disk).await?,
278            encryption_policy: disk.encryption_policy,
279        })
280    }
281
282    Ok(match vmgs {
283        PetriVmgsResource::Disk(disk) => VmgsResource::Disk(convert_disk(disk).await?),
284        PetriVmgsResource::ReprovisionOnFailure(disk) => {
285            VmgsResource::ReprovisionOnFailure(convert_disk(disk).await?)
286        }
287        PetriVmgsResource::Reprovision(disk) => {
288            VmgsResource::Reprovision(convert_disk(disk).await?)
289        }
290        PetriVmgsResource::Ephemeral => VmgsResource::Ephemeral,
291    })
292}
293
294async fn petri_disk_to_openvmm(disk: &Disk) -> anyhow::Result<Resource<DiskHandleKind>> {
295    Ok(match disk {
296        Disk::Memory(len) => LayeredDiskHandle::single_layer(RamDiskLayerHandle {
297            len: Some(*len),
298            sector_size: None,
299        })
300        .into_resource(),
301        Disk::Differencing(DiskPath::Local(path)) => memdiff_disk(path).await?,
302        Disk::Differencing(DiskPath::Remote { url }) => memdiff_remote_disk(url)?,
303        Disk::Persistent(path) => {
304            open_disk_type(
305                path.as_ref(),
306                OpenDiskOptions {
307                    read_only: false,
308                    direct: false,
309                },
310            )
311            .await?
312        }
313        Disk::Temporary(path) => {
314            open_disk_type(
315                path.as_ref(),
316                OpenDiskOptions {
317                    read_only: false,
318                    direct: false,
319                },
320            )
321            .await?
322        }
323    })
324}