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