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::PetriVmOpenVmm;
17
18use super::ProcessorTopology;
19use crate::Firmware;
20use crate::PetriLogFile;
21use crate::PetriLogSource;
22use crate::PetriVm;
23use crate::PetriVmConfig;
24use crate::disk_image::AgentImage;
25use crate::linux_direct_serial_agent::LinuxDirectSerialAgent;
26use crate::openhcl_diag::OpenHclDiagHandler;
27use anyhow::Context;
28use async_trait::async_trait;
29use disk_backend_resources::LayeredDiskHandle;
30use disk_backend_resources::layer::DiskLayerHandle;
31use disk_backend_resources::layer::RamDiskLayerHandle;
32use framebuffer::FramebufferAccess;
33use get_resources::ged::FirmwareEvent;
34use guid::Guid;
35use hvlite_defs::config::Config;
36use hvlite_helpers::disk::open_disk_type;
37use hyperv_ic_resources::shutdown::ShutdownRpc;
38use mesh::Receiver;
39use mesh::Sender;
40use net_backend_resources::mac_address::MacAddress;
41use pal_async::DefaultDriver;
42use pal_async::socket::PolledSocket;
43use pal_async::task::Task;
44use petri_artifacts_common::tags::MachineArch;
45use petri_artifacts_common::tags::OsFlavor;
46use petri_artifacts_core::ArtifactResolver;
47use petri_artifacts_core::ResolvedArtifact;
48use pipette_client::PipetteClient;
49use std::path::PathBuf;
50use tempfile::TempPath;
51use unix_socket::UnixListener;
52use vm_resource::IntoResource;
53use vm_resource::Resource;
54use vm_resource::kind::DiskHandleKind;
55use vtl2_settings_proto::Vtl2Settings;
56
57/// The instance guid used for all of our SCSI drives.
58pub(crate) const SCSI_INSTANCE: Guid = guid::guid!("27b553e8-8b39-411b-a55f-839971a7884f");
59
60/// The instance guid for the NVMe controller automatically added for boot media.
61pub(crate) const BOOT_NVME_INSTANCE: Guid = guid::guid!("92bc8346-718b-449a-8751-edbf3dcd27e4");
62
63/// The instance guid for the MANA nic automatically added when specifying `PetriVmConfigOpenVmm::with_nic`
64const MANA_INSTANCE: Guid = guid::guid!("f9641cf4-d915-4743-a7d8-efa75db7b85a");
65
66/// The namespace ID for the NVMe controller automatically added for boot media.
67pub(crate) const BOOT_NVME_NSID: u32 = 37;
68
69/// The LUN ID for the NVMe controller automatically added for boot media.
70pub(crate) const BOOT_NVME_LUN: u32 = 1;
71
72/// The MAC address used by the NIC assigned with [`PetriVmConfigOpenVmm::with_nic`].
73pub const NIC_MAC_ADDRESS: MacAddress = MacAddress::new([0x00, 0x15, 0x5D, 0x12, 0x12, 0x12]);
74
75/// The set of artifacts and resources needed to instantiate a
76/// [`PetriVmConfigOpenVmm`].
77pub struct PetriVmArtifactsOpenVmm {
78    firmware: Firmware,
79    arch: MachineArch,
80    agent_image: AgentImage,
81    openhcl_agent_image: Option<AgentImage>,
82    openvmm_path: ResolvedArtifact,
83}
84
85impl PetriVmArtifactsOpenVmm {
86    /// Resolves the artifacts needed to instantiate a [`PetriVmConfigOpenVmm`].
87    ///
88    /// Returns `None` if the supplied configuration is not supported on this platform.
89    pub fn new(
90        resolver: &ArtifactResolver<'_>,
91        firmware: Firmware,
92        arch: MachineArch,
93    ) -> Option<Self> {
94        if arch != MachineArch::host() {
95            return None;
96        }
97        if firmware.is_openhcl() {
98            // Only limited support for using OpenHCL on OpenVMM.
99            if !cfg!(windows) || arch != MachineArch::X86_64 {
100                return None;
101            }
102        }
103        let agent_image = AgentImage::new(resolver, arch, firmware.os_flavor());
104        let openhcl_agent_image = if firmware.is_openhcl() {
105            Some(AgentImage::new(resolver, arch, OsFlavor::Linux))
106        } else {
107            None
108        };
109        Some(Self {
110            firmware,
111            arch,
112            agent_image,
113            openhcl_agent_image,
114            openvmm_path: resolver
115                .require(petri_artifacts_vmm_test::artifacts::OPENVMM_NATIVE)
116                .erase(),
117        })
118    }
119}
120
121/// Configuration state for a test VM.
122pub struct PetriVmConfigOpenVmm {
123    // Direct configuration related information.
124    firmware: Firmware,
125    arch: MachineArch,
126    config: Config,
127
128    // Runtime resources
129    resources: PetriVmResourcesOpenVmm,
130
131    // Logging
132    openvmm_log_file: PetriLogFile,
133
134    // Resources that are only used during startup.
135    ged: Option<get_resources::ged::GuestEmulationDeviceHandle>,
136    vtl2_settings: Option<Vtl2Settings>,
137    framebuffer_access: Option<FramebufferAccess>,
138}
139
140#[async_trait]
141impl PetriVmConfig for PetriVmConfigOpenVmm {
142    async fn run_without_agent(self: Box<Self>) -> anyhow::Result<Box<dyn PetriVm>> {
143        Ok(Box::new(Self::run_without_agent(*self).await?))
144    }
145
146    async fn run_with_lazy_pipette(mut self: Box<Self>) -> anyhow::Result<Box<dyn PetriVm>> {
147        Ok(Box::new(Self::run_with_lazy_pipette(*self).await?))
148    }
149
150    async fn run(self: Box<Self>) -> anyhow::Result<(Box<dyn PetriVm>, PipetteClient)> {
151        let (vm, client) = Self::run(*self).await?;
152        Ok((Box::new(vm), client))
153    }
154
155    fn with_secure_boot(self: Box<Self>) -> Box<dyn PetriVmConfig> {
156        Box::new(Self::with_secure_boot(*self))
157    }
158
159    fn with_windows_secure_boot_template(self: Box<Self>) -> Box<dyn PetriVmConfig> {
160        Box::new(Self::with_windows_secure_boot_template(*self))
161    }
162
163    fn with_uefi_ca_secure_boot_template(self: Box<Self>) -> Box<dyn PetriVmConfig> {
164        Box::new(Self::with_uefi_ca_secure_boot_template(*self))
165    }
166
167    fn with_processor_topology(
168        self: Box<Self>,
169        topology: ProcessorTopology,
170    ) -> Box<dyn PetriVmConfig> {
171        Box::new(Self::with_processor_topology(*self, topology))
172    }
173
174    fn with_custom_openhcl(self: Box<Self>, artifact: ResolvedArtifact) -> Box<dyn PetriVmConfig> {
175        Box::new(Self::with_custom_openhcl(*self, artifact))
176    }
177
178    fn with_openhcl_command_line(self: Box<Self>, command_line: &str) -> Box<dyn PetriVmConfig> {
179        Box::new(Self::with_openhcl_command_line(*self, command_line))
180    }
181
182    fn with_agent_file(
183        self: Box<Self>,
184        name: &str,
185        artifact: ResolvedArtifact,
186    ) -> Box<dyn PetriVmConfig> {
187        Box::new(Self::with_agent_file(*self, name, artifact))
188    }
189
190    fn with_openhcl_agent_file(
191        self: Box<Self>,
192        name: &str,
193        artifact: ResolvedArtifact,
194    ) -> Box<dyn PetriVmConfig> {
195        Box::new(Self::with_openhcl_agent_file(*self, name, artifact))
196    }
197
198    fn with_uefi_frontpage(self: Box<Self>, enable: bool) -> Box<dyn PetriVmConfig> {
199        Box::new(Self::with_uefi_frontpage(*self, enable))
200    }
201
202    fn with_vmbus_redirect(self: Box<Self>, _: bool) -> Box<dyn PetriVmConfig> {
203        Box::new(Self::with_vmbus_redirect(*self))
204    }
205
206    fn os_flavor(&self) -> OsFlavor {
207        self.firmware.os_flavor()
208    }
209}
210
211/// Various channels and resources used to interact with the VM while it is running.
212struct PetriVmResourcesOpenVmm {
213    log_stream_tasks: Vec<Task<anyhow::Result<()>>>,
214    firmware_event_recv: Receiver<FirmwareEvent>,
215    shutdown_ic_send: Sender<ShutdownRpc>,
216    kvp_ic_send: Sender<hyperv_ic_resources::kvp::KvpConnectRpc>,
217    expected_boot_event: Option<FirmwareEvent>,
218    ged_send: Option<Sender<get_resources::ged::GuestEmulationRequest>>,
219    pipette_listener: PolledSocket<UnixListener>,
220    vtl2_pipette_listener: Option<PolledSocket<UnixListener>>,
221    openhcl_diag_handler: Option<OpenHclDiagHandler>,
222    linux_direct_serial_agent: Option<LinuxDirectSerialAgent>,
223
224    // Externally injected management stuff also needed at runtime.
225    driver: DefaultDriver,
226    agent_image: AgentImage,
227    openhcl_agent_image: Option<AgentImage>,
228    openvmm_path: ResolvedArtifact,
229    output_dir: PathBuf,
230    log_source: PetriLogSource,
231
232    // TempPaths that cannot be dropped until the end.
233    vtl2_vsock_path: Option<TempPath>,
234    _vmbus_vsock_path: TempPath,
235}
236
237impl PetriVmConfigOpenVmm {
238    /// Get the OS that the VM will boot into.
239    pub fn os_flavor(&self) -> OsFlavor {
240        self.firmware.os_flavor()
241    }
242}
243
244fn memdiff_disk_from_artifact(
245    artifact: &ResolvedArtifact,
246) -> anyhow::Result<Resource<DiskHandleKind>> {
247    let path = artifact.as_ref();
248    let disk = open_disk_type(path, true)
249        .with_context(|| format!("failed to open disk: {}", path.display()))?;
250    Ok(LayeredDiskHandle {
251        layers: vec![
252            RamDiskLayerHandle { len: None }.into_resource().into(),
253            DiskLayerHandle(disk).into_resource().into(),
254        ],
255    }
256    .into_resource())
257}