1mod construct;
12#[cfg(target_os = "linux")]
13mod hugetlb;
14mod modify;
15mod runtime;
16mod start;
17
18#[cfg(target_os = "linux")]
19pub use hugetlb::HUGETLB_2MB_PAGE_SIZE;
20#[cfg(target_os = "linux")]
21pub use hugetlb::ensure_2mb_hugetlb_pages;
22pub use runtime::OpenVmmFramebufferAccess;
23pub use runtime::OpenVmmInspector;
24pub use runtime::PetriVmOpenVmm;
25
26use crate::Disk;
27use crate::DiskPath;
28use crate::Firmware;
29use crate::ModifyFn;
30use crate::OpenHclServicingFlags;
31use crate::OpenvmmLogConfig;
32use crate::PetriLogFile;
33use crate::PetriVmConfig;
34use crate::PetriVmResources;
35use crate::PetriVmRuntimeConfig;
36use crate::PetriVmgsDisk;
37use crate::PetriVmgsResource;
38use crate::PetriVmmBackend;
39use crate::VmmQuirks;
40use crate::linux_direct_serial_agent::LinuxDirectSerialAgent;
41use crate::vm::PetriVmProperties;
42use anyhow::Context;
43use async_trait::async_trait;
44use disk_backend_resources::DiskLayerDescription;
45use disk_backend_resources::LayeredDiskHandle;
46use disk_backend_resources::layer::DiskLayerHandle;
47use disk_backend_resources::layer::RamDiskLayerHandle;
48use disk_backend_resources::layer::SqliteAutoCacheDiskLayerHandle;
49use get_resources::ged::FirmwareEvent;
50use guid::Guid;
51use hyperv_ic_resources::shutdown::ShutdownRpc;
52use mesh::Receiver;
53use mesh::Sender;
54use net_backend_resources::mac_address::MacAddress;
55use openvmm_defs::config::Config;
56use openvmm_helpers::disk::OpenDiskOptions;
57use openvmm_helpers::disk::open_disk_type;
58use pal_async::DefaultDriver;
59use pal_async::socket::PolledSocket;
60use pal_async::task::Task;
61use petri_artifacts_common::tags::GuestQuirksInner;
62use petri_artifacts_common::tags::MachineArch;
63use petri_artifacts_core::ArtifactResolver;
64use petri_artifacts_core::ResolvedArtifact;
65use std::path::Path;
66use std::path::PathBuf;
67use std::sync::Arc;
68use std::time::Duration;
69use tempfile::TempPath;
70use unix_socket::UnixListener;
71use vm_resource::IntoResource;
72use vm_resource::Resource;
73use vm_resource::kind::DiskHandleKind;
74use vmgs_resources::VmgsDisk;
75use vmgs_resources::VmgsResource;
76
77const MANA_INSTANCE: Guid = guid::guid!("f9641cf4-d915-4743-a7d8-efa75db7b85a");
79
80pub const NIC_MAC_ADDRESS: MacAddress = MacAddress::new([0x00, 0x15, 0x5D, 0x12, 0x12, 0x12]);
82
83#[derive(Debug)]
85pub struct OpenVmmPetriBackend {
86 openvmm_path: ResolvedArtifact,
87}
88
89#[async_trait]
90impl PetriVmmBackend for OpenVmmPetriBackend {
91 type VmmConfig = PetriVmConfigOpenVmm;
92 type VmRuntime = PetriVmOpenVmm;
93
94 fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool {
95 arch == MachineArch::host()
96 && !(firmware.is_openhcl() && (!cfg!(windows) || arch == MachineArch::Aarch64))
97 && !(firmware.is_pcat() && arch == MachineArch::Aarch64)
98 }
99
100 fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks) {
101 (
102 firmware.quirks().openvmm,
103 VmmQuirks {
104 flaky_boot: firmware.is_pcat().then_some(Duration::from_secs(15)),
106 },
107 )
108 }
109
110 fn default_servicing_flags() -> OpenHclServicingFlags {
111 OpenHclServicingFlags {
112 enable_nvme_keepalive: true,
113 enable_mana_keepalive: true,
114 override_version_checks: false,
115 stop_timeout_hint_secs: None,
116 }
117 }
118
119 fn create_guest_dump_disk() -> anyhow::Result<
120 Option<(
121 Arc<TempPath>,
122 Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
123 )>,
124 > {
125 Ok(None) }
127
128 fn new(resolver: &ArtifactResolver<'_>) -> Self {
129 OpenVmmPetriBackend {
130 openvmm_path: resolver
131 .require(petri_artifacts_vmm_test::artifacts::OPENVMM_NATIVE)
132 .erase(),
133 }
134 }
135
136 async fn run(
137 self,
138 config: PetriVmConfig,
139 modify_vmm_config: Option<ModifyFn<Self::VmmConfig>>,
140 resources: &PetriVmResources,
141 properties: PetriVmProperties,
142 ) -> anyhow::Result<(Self::VmRuntime, PetriVmRuntimeConfig)> {
143 let mut config =
144 PetriVmConfigOpenVmm::new(&self.openvmm_path, config, resources, properties).await?;
145
146 if let Some(f) = modify_vmm_config {
147 config = f.0(config);
148 }
149
150 config.run().await
151 }
152}
153
154pub struct PetriVmConfigOpenVmm {
156 runtime_config: PetriVmRuntimeConfig,
158 arch: MachineArch,
159 host_log_levels: Option<OpenvmmLogConfig>,
160 config: Config,
161
162 mesh: mesh_process::Mesh,
164
165 resources: PetriVmResourcesOpenVmm,
167
168 openvmm_log_file: PetriLogFile,
170
171 memory_backing_file: Option<PathBuf>,
173
174 requested_private_memory: Option<bool>,
180
181 ged: Option<get_resources::ged::GuestEmulationDeviceHandle>,
183 framebuffer_view: Option<framebuffer::View>,
184
185 pending_iommu: Vec<(String, openvmm_defs::config::PcieIommuConfig)>,
188}
189struct PetriVmResourcesOpenVmm {
191 log_stream_tasks: Vec<Task<anyhow::Result<()>>>,
192 firmware_event_recv: Receiver<FirmwareEvent>,
193 ipmi_sel_event_recv: Receiver<get_resources::ged::IpmiSelEvent>,
194 shutdown_ic_send: Option<Sender<ShutdownRpc>>,
195 kvp_ic_send: Option<Sender<hyperv_ic_resources::kvp::KvpConnectRpc>>,
196 ged_send: Option<Sender<get_resources::ged::GuestEmulationRequest>>,
197 pipette_listener: PolledSocket<UnixListener>,
198 vtl2_pipette_listener: Option<PolledSocket<UnixListener>>,
199 linux_direct_serial_agent: Option<LinuxDirectSerialAgent>,
200
201 tcp_pipette_port: Option<mesh::OneshotReceiver<u16>>,
207
208 driver: DefaultDriver,
210 openvmm_path: ResolvedArtifact,
211 output_dir: PathBuf,
212
213 vtl2_vsock_path: Option<TempPath>,
215 _vsock_path: TempPath,
216
217 properties: PetriVmProperties,
219
220 #[cfg(windows)]
224 _switch_ports: Vec<vmswitch::kernel::SwitchPort>,
225}
226
227#[cfg(windows)]
239pub fn find_switch() -> Option<Guid> {
240 if vmswitch::hcn::Network::open(&vmswitch::hcn::DEFAULT_SWITCH).is_ok() {
241 return Some(vmswitch::hcn::DEFAULT_SWITCH);
242 }
243 let networks = match vmswitch::hcn::enumerate_networks() {
244 Ok(n) => n,
245 Err(e) => {
246 tracing::warn!(
247 error = &e as &dyn std::error::Error,
248 "failed to enumerate HCN networks"
249 );
250 return None;
251 }
252 };
253 networks.into_iter().find(|guid| {
254 if let Err(e) = vmswitch::hcn::Network::open(guid) {
255 tracing::debug!(
256 %guid,
257 error = &e as &dyn std::error::Error,
258 "skipping unopenable HCN network"
259 );
260 false
261 } else {
262 true
263 }
264 })
265}
266
267#[cfg(not(windows))]
271pub fn find_switch() -> Option<Guid> {
272 None
273}
274
275async fn memdiff_disk(path: &Path) -> anyhow::Result<Resource<DiskHandleKind>> {
276 let disk = open_disk_type(
277 path,
278 OpenDiskOptions {
279 read_only: true,
280 direct: false,
281 },
282 )
283 .await
284 .with_context(|| format!("failed to open disk: {}", path.display()))?;
285 Ok(LayeredDiskHandle {
286 layers: vec![
287 RamDiskLayerHandle {
288 len: None,
289 sector_size: None,
290 }
291 .into_resource()
292 .into(),
293 DiskLayerHandle(disk).into_resource().into(),
294 ],
295 }
296 .into_resource())
297}
298
299fn memdiff_remote_disk(url: &str) -> anyhow::Result<Resource<DiskHandleKind>> {
300 let url_path = url.split(['?', '#']).next().unwrap_or(url);
302 let format = if url_path.ends_with(".vhd") || url_path.ends_with(".vmgs") {
303 disk_backend_resources::BlobDiskFormat::FixedVhd1
304 } else {
305 disk_backend_resources::BlobDiskFormat::Flat
306 };
307
308 let cache_dir = super::petri_disk_cache_dir();
309
310 let cache_key = match format {
316 disk_backend_resources::BlobDiskFormat::FixedVhd1 => None,
317 disk_backend_resources::BlobDiskFormat::Flat => {
318 Some(url_path.rsplit('/').next().unwrap_or(url_path).to_owned())
319 }
320 };
321
322 Ok(LayeredDiskHandle {
323 layers: vec![
324 RamDiskLayerHandle {
325 len: None,
326 sector_size: None,
327 }
328 .into_resource()
329 .into(),
330 DiskLayerDescription {
331 read_cache: true,
332 write_through: false,
333 layer: SqliteAutoCacheDiskLayerHandle {
334 cache_path: cache_dir,
335 cache_key,
336 }
337 .into_resource(),
338 },
339 DiskLayerHandle(
340 disk_backend_resources::BlobDiskHandle {
341 url: url.to_owned(),
342 format,
343 }
344 .into_resource(),
345 )
346 .into_resource()
347 .into(),
348 ],
349 }
350 .into_resource())
351}
352
353async fn memdiff_vmgs(vmgs: &PetriVmgsResource) -> anyhow::Result<VmgsResource> {
354 async fn convert_disk(disk: &PetriVmgsDisk) -> anyhow::Result<VmgsDisk> {
355 Ok(VmgsDisk {
356 disk: petri_disk_to_openvmm(&disk.disk).await?,
357 encryption_policy: disk.encryption_policy,
358 })
359 }
360
361 Ok(match vmgs {
362 PetriVmgsResource::Disk(disk) => VmgsResource::Disk(convert_disk(disk).await?),
363 PetriVmgsResource::ReprovisionOnFailure(disk) => {
364 VmgsResource::ReprovisionOnFailure(convert_disk(disk).await?)
365 }
366 PetriVmgsResource::Reprovision(disk) => {
367 VmgsResource::Reprovision(convert_disk(disk).await?)
368 }
369 PetriVmgsResource::Ephemeral => VmgsResource::Ephemeral,
370 })
371}
372
373async fn petri_disk_to_openvmm(disk: &Disk) -> anyhow::Result<Resource<DiskHandleKind>> {
374 Ok(match disk {
375 Disk::Memory(len) => LayeredDiskHandle::single_layer(RamDiskLayerHandle {
376 len: Some(*len),
377 sector_size: None,
378 })
379 .into_resource(),
380 Disk::Differencing(DiskPath::Local(path)) => memdiff_disk(path).await?,
381 Disk::Differencing(DiskPath::Remote { url }) => memdiff_remote_disk(url)?,
382 Disk::Persistent(path) => {
383 open_disk_type(
384 path.as_ref(),
385 OpenDiskOptions {
386 read_only: false,
387 direct: false,
388 },
389 )
390 .await?
391 }
392 Disk::Temporary(path) => {
393 open_disk_type(
394 path.as_ref(),
395 OpenDiskOptions {
396 read_only: false,
397 direct: false,
398 },
399 )
400 .await?
401 }
402 })
403}