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::BootDeviceType;
21use crate::Firmware;
22use crate::OpenHclServicingFlags;
23use crate::OpenvmmLogConfig;
24use crate::PetriDiskType;
25use crate::PetriLogFile;
26use crate::PetriVmConfig;
27use crate::PetriVmResources;
28use crate::PetriVmRuntimeConfig;
29use crate::PetriVmgsDisk;
30use crate::PetriVmgsResource;
31use crate::PetriVmmBackend;
32use crate::VmmQuirks;
33use crate::disk_image::AgentImage;
34use crate::linux_direct_serial_agent::LinuxDirectSerialAgent;
35use anyhow::Context;
36use async_trait::async_trait;
37use disk_backend_resources::LayeredDiskHandle;
38use disk_backend_resources::layer::DiskLayerHandle;
39use disk_backend_resources::layer::RamDiskLayerHandle;
40use get_resources::ged::FirmwareEvent;
41use guid::Guid;
42use hyperv_ic_resources::shutdown::ShutdownRpc;
43use mesh::Receiver;
44use mesh::Sender;
45use net_backend_resources::mac_address::MacAddress;
46use openvmm_defs::config::Config;
47use openvmm_helpers::disk::open_disk_type;
48use pal_async::DefaultDriver;
49use pal_async::socket::PolledSocket;
50use pal_async::task::Task;
51use petri_artifacts_common::tags::GuestQuirksInner;
52use petri_artifacts_common::tags::MachineArch;
53use petri_artifacts_common::tags::OsFlavor;
54use petri_artifacts_core::ArtifactResolver;
55use petri_artifacts_core::ResolvedArtifact;
56use std::path::Path;
57use std::path::PathBuf;
58use std::sync::Arc;
59use std::time::Duration;
60use storvsp_resources::ScsiControllerHandle;
61use tempfile::TempPath;
62use unix_socket::UnixListener;
63use vm_resource::IntoResource;
64use vm_resource::Resource;
65use vm_resource::kind::DiskHandleKind;
66use vmgs_resources::VmgsDisk;
67use vmgs_resources::VmgsResource;
68use vtl2_settings_proto::Vtl2Settings;
69
70/// The instance guid used for all of our SCSI drives.
71pub(crate) const SCSI_INSTANCE: Guid = guid::guid!("27b553e8-8b39-411b-a55f-839971a7884f");
72
73/// The instance guid for the NVMe controller automatically added for boot media
74/// for paravisor storage translation.
75pub(crate) const PARAVISOR_BOOT_NVME_INSTANCE: Guid =
76    guid::guid!("92bc8346-718b-449a-8751-edbf3dcd27e4");
77
78/// The instance guid for the NVMe controller automatically added for boot media.
79pub(crate) const BOOT_NVME_INSTANCE: Guid = guid::guid!("e23a04e2-90f5-4852-bc9d-e7ac691b756c");
80
81/// The instance guid for the MANA nic automatically added when specifying `PetriVmConfigOpenVmm::with_nic`
82const MANA_INSTANCE: Guid = guid::guid!("f9641cf4-d915-4743-a7d8-efa75db7b85a");
83
84/// The namespace ID for the NVMe controller automatically added for boot media.
85pub(crate) const BOOT_NVME_NSID: u32 = 37;
86
87/// The LUN ID for the NVMe controller automatically added for boot media.
88pub(crate) const BOOT_NVME_LUN: u32 = 1;
89
90/// The MAC address used by the NIC assigned with [`PetriVmConfigOpenVmm::with_nic`].
91pub const NIC_MAC_ADDRESS: MacAddress = MacAddress::new([0x00, 0x15, 0x5D, 0x12, 0x12, 0x12]);
92
93/// OpenVMM Petri Backend
94pub struct OpenVmmPetriBackend {
95    openvmm_path: ResolvedArtifact,
96}
97
98#[async_trait]
99impl PetriVmmBackend for OpenVmmPetriBackend {
100    type VmmConfig = PetriVmConfigOpenVmm;
101    type VmRuntime = PetriVmOpenVmm;
102
103    fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool {
104        arch == MachineArch::host()
105            && !(firmware.is_openhcl() && (!cfg!(windows) || arch == MachineArch::Aarch64))
106            && !(firmware.is_pcat() && arch == MachineArch::Aarch64)
107    }
108
109    fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks) {
110        (
111            firmware.quirks().openvmm,
112            VmmQuirks {
113                // Workaround for #1684
114                flaky_boot: firmware.is_pcat().then_some(Duration::from_secs(15)),
115            },
116        )
117    }
118
119    fn default_servicing_flags() -> OpenHclServicingFlags {
120        OpenHclServicingFlags {
121            enable_nvme_keepalive: true,
122            enable_mana_keepalive: true,
123            override_version_checks: false,
124            stop_timeout_hint_secs: None,
125        }
126    }
127
128    fn create_guest_dump_disk() -> anyhow::Result<
129        Option<(
130            Arc<TempPath>,
131            Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
132        )>,
133    > {
134        Ok(None) // TODO #2403
135    }
136
137    fn new(resolver: &ArtifactResolver<'_>) -> Self {
138        OpenVmmPetriBackend {
139            openvmm_path: resolver
140                .require(petri_artifacts_vmm_test::artifacts::OPENVMM_NATIVE)
141                .erase(),
142        }
143    }
144
145    async fn run(
146        self,
147        config: PetriVmConfig,
148        modify_vmm_config: Option<impl FnOnce(PetriVmConfigOpenVmm) -> PetriVmConfigOpenVmm + Send>,
149        resources: &PetriVmResources,
150    ) -> anyhow::Result<(Self::VmRuntime, PetriVmRuntimeConfig)> {
151        let mut config = PetriVmConfigOpenVmm::new(&self.openvmm_path, config, resources).await?;
152
153        if let Some(f) = modify_vmm_config {
154            config = f(config);
155        }
156
157        config.run().await
158    }
159}
160
161/// Configuration state for a test VM.
162pub struct PetriVmConfigOpenVmm {
163    // Direct configuration related information.
164    firmware: Firmware,
165    arch: MachineArch,
166    host_log_levels: Option<OpenvmmLogConfig>,
167    config: Config,
168    boot_device_type: BootDeviceType,
169
170    // Mesh host
171    mesh: mesh_process::Mesh,
172
173    // Runtime resources
174    resources: PetriVmResourcesOpenVmm,
175
176    // Logging
177    openvmm_log_file: PetriLogFile,
178
179    // Resources that are only used during startup.
180    /// Single VMBus SCSI controller shared for all VTL0 disks added by petri.
181    petri_vtl0_scsi: ScsiControllerHandle,
182
183    ged: Option<get_resources::ged::GuestEmulationDeviceHandle>,
184    framebuffer_view: Option<framebuffer::View>,
185
186    vtl2_settings: Option<Vtl2Settings>,
187}
188/// Various channels and resources used to interact with the VM while it is running.
189struct PetriVmResourcesOpenVmm {
190    log_stream_tasks: Vec<Task<anyhow::Result<()>>>,
191    firmware_event_recv: Receiver<FirmwareEvent>,
192    shutdown_ic_send: Sender<ShutdownRpc>,
193    kvp_ic_send: Sender<hyperv_ic_resources::kvp::KvpConnectRpc>,
194    ged_send: Option<Sender<get_resources::ged::GuestEmulationRequest>>,
195    pipette_listener: PolledSocket<UnixListener>,
196    vtl2_pipette_listener: Option<PolledSocket<UnixListener>>,
197    linux_direct_serial_agent: Option<LinuxDirectSerialAgent>,
198
199    // Externally injected management stuff also needed at runtime.
200    driver: DefaultDriver,
201    agent_image: Option<AgentImage>,
202    openhcl_agent_image: Option<AgentImage>,
203    openvmm_path: ResolvedArtifact,
204    output_dir: PathBuf,
205
206    // TempPaths that cannot be dropped until the end.
207    vtl2_vsock_path: Option<TempPath>,
208    _vmbus_vsock_path: TempPath,
209}
210
211impl PetriVmConfigOpenVmm {
212    /// Get the OS that the VM will boot into.
213    pub fn os_flavor(&self) -> OsFlavor {
214        self.firmware.os_flavor()
215    }
216}
217
218fn memdiff_disk(path: &Path) -> anyhow::Result<Resource<DiskHandleKind>> {
219    let disk = open_disk_type(path, true)
220        .with_context(|| format!("failed to open disk: {}", path.display()))?;
221    Ok(LayeredDiskHandle {
222        layers: vec![
223            RamDiskLayerHandle { len: None }.into_resource().into(),
224            DiskLayerHandle(disk).into_resource().into(),
225        ],
226    }
227    .into_resource())
228}
229
230fn memdiff_vmgs(vmgs: &PetriVmgsResource) -> anyhow::Result<VmgsResource> {
231    let convert_disk = |disk: &PetriVmgsDisk| -> anyhow::Result<VmgsDisk> {
232        Ok(VmgsDisk {
233            disk: match &disk.disk {
234                PetriDiskType::Memory => LayeredDiskHandle::single_layer(RamDiskLayerHandle {
235                    len: Some(vmgs_format::VMGS_DEFAULT_CAPACITY),
236                })
237                .into_resource(),
238                PetriDiskType::Differencing(path) => memdiff_disk(path)?,
239                PetriDiskType::Persistent(path) => open_disk_type(path, false)?,
240            },
241            encryption_policy: disk.encryption_policy,
242        })
243    };
244
245    Ok(match vmgs {
246        PetriVmgsResource::Disk(disk) => VmgsResource::Disk(convert_disk(disk)?),
247        PetriVmgsResource::ReprovisionOnFailure(disk) => {
248            VmgsResource::ReprovisionOnFailure(convert_disk(disk)?)
249        }
250        PetriVmgsResource::Reprovision(disk) => VmgsResource::Reprovision(convert_disk(disk)?),
251        PetriVmgsResource::Ephemeral => VmgsResource::Ephemeral,
252    })
253}