1mod construct;
12#[cfg(target_os = "linux")]
13mod hugetlb;
14mod modify;
15mod mshv;
16mod runtime;
17mod start;
18
19#[cfg(target_os = "linux")]
20pub use hugetlb::HUGETLB_2MB_PAGE_SIZE;
21#[cfg(target_os = "linux")]
22pub use hugetlb::ensure_2mb_hugetlb_pages;
23pub use runtime::OpenVmmFramebufferAccess;
24pub use runtime::OpenVmmInspector;
25pub use runtime::PetriVmOpenVmm;
26
27use crate::Disk;
28use crate::DiskPath;
29use crate::Firmware;
30use crate::ModifyFn;
31use crate::OpenHclServicingFlags;
32use crate::OpenvmmLogConfig;
33use crate::PetriLogFile;
34use crate::PetriVmConfig;
35use crate::PetriVmResources;
36use crate::PetriVmRuntimeConfig;
37use crate::PetriVmgsDisk;
38use crate::PetriVmgsResource;
39use crate::PetriVmmBackend;
40use crate::VmmQuirks;
41use crate::linux_direct_serial_agent::LinuxDirectSerialAgent;
42use crate::vm::PetriVmProperties;
43use anyhow::Context;
44use async_trait::async_trait;
45use chipset_resources::acpi_power_button::PowerButtonRpc;
46use disk_backend_resources::DiskLayerDescription;
47use disk_backend_resources::LayeredDiskHandle;
48use disk_backend_resources::layer::DiskLayerHandle;
49use disk_backend_resources::layer::RamDiskLayerHandle;
50use disk_backend_resources::layer::SqliteAutoCacheDiskLayerHandle;
51use disk_backend_resources::layer::SqliteDiskLayerFormatParams;
52use disk_backend_resources::layer::SqliteDiskLayerHandle;
53use get_resources::ged::FirmwareEvent;
54use guid::Guid;
55use hyperv_ic_resources::shutdown::ShutdownRpc;
56use mesh::Receiver;
57use mesh::Sender;
58use net_backend_resources::mac_address::MacAddress;
59use openvmm_defs::config::Config;
60use openvmm_helpers::disk::OpenDiskOptions;
61use openvmm_helpers::disk::open_disk_type;
62use pal_async::DefaultDriver;
63use pal_async::socket::PolledSocket;
64use pal_async::task::Task;
65use petri_artifacts_common::tags::GuestQuirksInner;
66use petri_artifacts_common::tags::MachineArch;
67use petri_artifacts_core::ArtifactResolver;
68use petri_artifacts_core::ResolvedArtifact;
69use std::path::Path;
70use std::path::PathBuf;
71use std::sync::Arc;
72use std::time::Duration;
73use tempfile::TempPath;
74use unix_socket::UnixListener;
75use vm_resource::IntoResource;
76use vm_resource::Resource;
77use vm_resource::kind::DiskHandleKind;
78use vm_resource::kind::VirtioDeviceHandle;
79use vmgs_resources::VmgsDisk;
80use vmgs_resources::VmgsResource;
81
82const MANA_INSTANCE: Guid = guid::guid!("f9641cf4-d915-4743-a7d8-efa75db7b85a");
84
85pub const NIC_MAC_ADDRESS: MacAddress = MacAddress::new([0x00, 0x15, 0x5D, 0x12, 0x12, 0x12]);
87
88#[derive(Debug)]
90pub struct OpenVmmPetriBackend {
91 openvmm_path: ResolvedArtifact,
92}
93
94impl crate::PetriVmBuilder<OpenVmmPetriBackend> {
95 pub fn with_virtio_console(mut self) -> Self {
101 assert!(
102 matches!(
103 self.config.firmware,
104 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. }
105 ) && self.config.firmware.os_flavor() == petri_artifacts_common::tags::OsFlavor::Linux
106 && self.config.firmware.isolation().is_none(),
107 "virtio-console requires Linux-direct or non-isolated Linux UEFI"
108 );
109 self.use_virtio_console = true;
110 self
111 }
112}
113
114impl OpenVmmPetriBackend {
115 pub fn from_binary(path: PathBuf) -> Self {
117 Self {
118 openvmm_path: ResolvedArtifact::from_local_path(path),
119 }
120 }
121}
122
123#[async_trait]
124impl PetriVmmBackend for OpenVmmPetriBackend {
125 type VmmConfig = PetriVmConfigOpenVmm;
126 type VmRuntime = PetriVmOpenVmm;
127 const SUPPORTS_VMBUS: bool = true;
128 const SUPPORTS_FILE_SHARING: bool = cfg!(any(windows, target_os = "linux"));
129
130 fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool {
131 !(firmware.is_openhcl() && (!cfg!(windows) || arch == MachineArch::Aarch64))
132 }
133
134 fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks) {
135 (
136 firmware.quirks().openvmm,
137 VmmQuirks {
138 flaky_boot: firmware.is_pcat().then_some(Duration::from_secs(15)),
140 },
141 )
142 }
143
144 fn default_servicing_flags() -> OpenHclServicingFlags {
145 OpenHclServicingFlags {
146 enable_nvme_keepalive: true,
147 enable_mana_keepalive: true,
148 override_version_checks: false,
149 stop_timeout_hint_secs: None,
150 }
151 }
152
153 fn create_guest_dump_disk() -> anyhow::Result<
154 Option<(
155 Arc<TempPath>,
156 Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
157 )>,
158 > {
159 Ok(None) }
161
162 fn build_custom_init_script(_pipette_path: &str) -> Option<String> {
163 None
164 }
165
166 fn new(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
167 assert_eq!(arch, MachineArch::host());
169 OpenVmmPetriBackend {
170 openvmm_path: resolver
171 .require(petri_artifacts_vmm_test::artifacts::OPENVMM_NATIVE)
172 .erase(),
173 }
174 }
175
176 async fn run(
177 self,
178 config: PetriVmConfig,
179 modify_vmm_config: Option<ModifyFn<Self::VmmConfig>>,
180 resources: &PetriVmResources,
181 properties: &PetriVmProperties,
182 ) -> anyhow::Result<(Self::VmRuntime, PetriVmRuntimeConfig)> {
183 let mut config =
184 PetriVmConfigOpenVmm::new(&self.openvmm_path, config, resources, properties).await?;
185
186 if let Some(f) = modify_vmm_config {
187 config = f.0(config);
188 }
189
190 config.run().await
191 }
192}
193
194pub struct OpenVmmRelaunchConfig {
197 builder: crate::PetriVmBuilder<OpenVmmPetriBackend>,
198 _diff_dir: tempfile::TempDir,
200 _watchdog_task: Option<Task<()>>,
202}
203
204impl crate::PetriVmBuilder<OpenVmmPetriBackend> {
205 pub fn prepare_openvmm_relaunch(self) -> anyhow::Result<OpenVmmRelaunchConfig> {
211 anyhow::ensure!(
212 self.modify_vmm_config.is_none(),
213 "OpenVMM relaunch configurations do not support modify_backend"
214 );
215
216 let mut builder = self.add_boot_disk()?.add_agent_disks()?;
217 if builder.uses_pipette_as_init() && builder.resources.prebuilt_initrd.is_none() {
218 builder.resources.prebuilt_initrd = Some(builder.prepare_initrd()?);
219 }
220
221 let diff_dir = tempfile::tempdir().context("failed to create disk write directory")?;
222 let config = &mut builder.config;
223 let drive_disks = config
224 .vmbus_storage_controllers
225 .values_mut()
226 .flat_map(|controller| controller.drives.values_mut())
227 .chain(config.pcie_nvme_drives.iter_mut().map(|d| &mut d.drive))
228 .chain(
229 config
230 .pcie_virtio_blk_drives
231 .iter_mut()
232 .map(|d| &mut d.drive),
233 )
234 .chain(
235 config
236 .firmware
237 .ide_controllers_mut()
238 .into_iter()
239 .flatten()
240 .flatten()
241 .flatten(),
242 )
243 .filter_map(|drive| drive.disk.as_mut());
244 let vmgs_disk = match &mut config.vmgs {
245 PetriVmgsResource::Disk(vmgs)
246 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
247 | PetriVmgsResource::ReprovisionSingle(vmgs) => Some(&mut vmgs.disk),
248 PetriVmgsResource::Ephemeral => None,
249 };
250 for (i, disk) in drive_disks.chain(vmgs_disk).enumerate() {
251 if let Disk::Differencing(base) = disk {
252 *disk = Disk::SqliteDifferencing {
253 base: base.clone(),
254 dbhd_path: diff_dir.path().join(format!("disk{i}.dbhd")),
255 };
256 }
257 }
258
259 let watchdog_task =
260 Self::start_watchdog_timer(&builder.resources.driver, builder.watchdog_timeout);
261 Ok(OpenVmmRelaunchConfig {
262 builder,
263 _diff_dir: diff_dir,
264 _watchdog_task: watchdog_task,
265 })
266 }
267}
268
269impl OpenVmmRelaunchConfig {
270 async fn config(
271 &self,
272 memory_backing_file: impl Into<PathBuf>,
273 ) -> anyhow::Result<PetriVmConfigOpenVmm> {
274 Ok(PetriVmConfigOpenVmm::new(
275 &self.builder.backend.openvmm_path,
276 self.builder.config.clone(),
277 &self.builder.resources,
278 &self.builder.properties(),
279 )
280 .await?
281 .with_memory_backing_file(memory_backing_file))
282 }
283
284 pub async fn launch(
286 &self,
287 memory_backing_file: impl Into<PathBuf>,
288 ) -> anyhow::Result<PetriVmOpenVmm> {
289 let (vm, _) = self.config(memory_backing_file).await?.run_core().await?;
290 Ok(vm)
291 }
292
293 pub async fn restore(
296 &self,
297 memory_backing_file: impl Into<PathBuf>,
298 saved_state: &[u8],
299 ) -> anyhow::Result<PetriVmOpenVmm> {
300 let (vm, _) = self
301 .config(memory_backing_file)
302 .await?
303 .run_restore(saved_state)
304 .await?;
305 Ok(vm)
306 }
307}
308
309pub struct PetriVmConfigOpenVmm {
311 guest_shares: std::collections::HashMap<String, crate::GuestShare>,
312 private_virtio_devices: Vec<(&'static str, Resource<VirtioDeviceHandle>)>,
313 runtime_config: PetriVmRuntimeConfig,
315 arch: MachineArch,
316 host_log_levels: Option<OpenvmmLogConfig>,
317 config: Config,
318 skip_save_restore: bool,
319
320 mesh: mesh_process::Mesh,
322
323 resources: PetriVmResourcesOpenVmm,
325
326 openvmm_log_file: PetriLogFile,
328
329 memory_backing_file: Option<PathBuf>,
331
332 requested_private_memory: Option<bool>,
338
339 ged: Option<get_resources::ged::GuestEmulationDeviceHandle>,
341 framebuffer_view: Option<framebuffer::View>,
342
343 pending_iommu: Vec<(String, openvmm_defs::config::PcieIommuConfig)>,
346}
347struct PetriVmResourcesOpenVmm {
349 log_stream_tasks: Vec<Task<anyhow::Result<()>>>,
350 firmware_event_recv: Receiver<FirmwareEvent>,
351 ipmi_sel_event_recv: Receiver<get_resources::ged::IpmiSelEvent>,
352 shutdown_ic_send: Option<Sender<ShutdownRpc>>,
353 acpi_power_button_send: Option<Sender<PowerButtonRpc>>,
354 kvp_ic_send: Option<Sender<hyperv_ic_resources::kvp::KvpConnectRpc>>,
355 ged_send: Option<Sender<get_resources::ged::GuestEmulationRequest>>,
356 pipette_listener: PolledSocket<UnixListener>,
357 vtl2_pipette_listener: Option<PolledSocket<UnixListener>>,
358 linux_direct_serial_agent: Option<LinuxDirectSerialAgent>,
359
360 tcp_pipette_port: Option<mesh::OneshotReceiver<u16>>,
366
367 driver: DefaultDriver,
369 openvmm_path: ResolvedArtifact,
370 output_dir: PathBuf,
371
372 vtl2_vsock_path: Option<TempPath>,
374 _vsock_path: TempPath,
375
376 properties: PetriVmProperties,
378
379 #[cfg(windows)]
383 _switch_ports: Vec<vmswitch::kernel::SwitchPort>,
384}
385
386#[cfg(windows)]
398pub fn find_switch() -> Option<Guid> {
399 if vmswitch::hcn::Network::open(&vmswitch::hcn::DEFAULT_SWITCH).is_ok() {
400 return Some(vmswitch::hcn::DEFAULT_SWITCH);
401 }
402 let networks = match vmswitch::hcn::enumerate_networks() {
403 Ok(n) => n,
404 Err(e) => {
405 tracing::warn!(
406 error = &e as &dyn std::error::Error,
407 "failed to enumerate HCN networks"
408 );
409 return None;
410 }
411 };
412 networks.into_iter().find(|guid| {
413 if let Err(e) = vmswitch::hcn::Network::open(guid) {
414 tracing::debug!(
415 %guid,
416 error = &e as &dyn std::error::Error,
417 "skipping unopenable HCN network"
418 );
419 false
420 } else {
421 true
422 }
423 })
424}
425
426#[cfg(not(windows))]
430pub fn find_switch() -> Option<Guid> {
431 None
432}
433
434fn ram_diff_layer() -> DiskLayerDescription {
435 RamDiskLayerHandle {
436 len: None,
437 sector_size: None,
438 }
439 .into_resource()
440 .into()
441}
442
443fn sqlite_diff_layer(dbhd_path: &Path) -> DiskLayerDescription {
446 SqliteDiskLayerHandle {
447 dbhd_path: dbhd_path.display().to_string(),
448 format_dbhd: (!dbhd_path.exists()).then_some(SqliteDiskLayerFormatParams {
449 logically_read_only: false,
450 len: None,
451 }),
452 }
453 .into_resource()
454 .into()
455}
456
457fn layered_disk(
458 diff: DiskLayerDescription,
459 base: Vec<DiskLayerDescription>,
460) -> Resource<DiskHandleKind> {
461 LayeredDiskHandle {
462 layers: std::iter::once(diff).chain(base).collect(),
463 }
464 .into_resource()
465}
466
467async fn memdiff_disk(path: &Path) -> anyhow::Result<Resource<DiskHandleKind>> {
469 Ok(layered_disk(
470 ram_diff_layer(),
471 local_base_layers(path).await?,
472 ))
473}
474
475fn memdiff_remote_disk(url: &str) -> anyhow::Result<Resource<DiskHandleKind>> {
477 Ok(layered_disk(ram_diff_layer(), remote_base_layers(url)))
478}
479
480async fn sqldiff_disk(path: &Path, dbhd_path: &Path) -> anyhow::Result<Resource<DiskHandleKind>> {
483 Ok(layered_disk(
484 sqlite_diff_layer(dbhd_path),
485 local_base_layers(path).await?,
486 ))
487}
488
489fn sqldiff_remote_disk(url: &str, dbhd_path: &Path) -> anyhow::Result<Resource<DiskHandleKind>> {
492 Ok(layered_disk(
493 sqlite_diff_layer(dbhd_path),
494 remote_base_layers(url),
495 ))
496}
497
498async fn local_base_layers(path: &Path) -> anyhow::Result<Vec<DiskLayerDescription>> {
499 let disk = open_disk_type(
500 path,
501 OpenDiskOptions {
502 read_only: true,
503 direct: false,
504 },
505 )
506 .await
507 .with_context(|| format!("failed to open disk: {}", path.display()))?;
508 Ok(vec![DiskLayerHandle(disk).into_resource().into()])
509}
510
511fn remote_base_layers(url: &str) -> Vec<DiskLayerDescription> {
512 let url_path = url.split(['?', '#']).next().unwrap_or(url);
514 let format = if url_path.ends_with(".vhd") || url_path.ends_with(".vmgs") {
515 disk_backend_resources::BlobDiskFormat::FixedVhd1
516 } else {
517 disk_backend_resources::BlobDiskFormat::Flat
518 };
519
520 let cache_dir = super::petri_disk_cache_dir();
521
522 let cache_key = match format {
528 disk_backend_resources::BlobDiskFormat::FixedVhd1 => None,
529 disk_backend_resources::BlobDiskFormat::Flat => {
530 Some(url_path.rsplit('/').next().unwrap_or(url_path).to_owned())
531 }
532 };
533
534 vec![
535 DiskLayerDescription {
536 read_cache: true,
537 write_through: false,
538 layer: SqliteAutoCacheDiskLayerHandle {
539 cache_path: cache_dir,
540 cache_key,
541 }
542 .into_resource(),
543 },
544 DiskLayerHandle(
545 disk_backend_resources::BlobDiskHandle {
546 url: url.to_owned(),
547 format,
548 }
549 .into_resource(),
550 )
551 .into_resource()
552 .into(),
553 ]
554}
555
556async fn memdiff_vmgs(vmgs: &PetriVmgsResource) -> anyhow::Result<VmgsResource> {
557 async fn convert_disk(disk: &PetriVmgsDisk) -> anyhow::Result<VmgsDisk> {
558 Ok(VmgsDisk {
559 disk: petri_disk_to_openvmm(&disk.disk).await?,
560 encryption_policy: disk.encryption_policy,
561 })
562 }
563
564 Ok(match vmgs {
565 PetriVmgsResource::Disk(disk) => VmgsResource::Disk(convert_disk(disk).await?),
566 PetriVmgsResource::ReprovisionOnFailure(disk) => {
567 VmgsResource::ReprovisionOnFailure(convert_disk(disk).await?)
568 }
569 PetriVmgsResource::ReprovisionSingle(disk) => {
570 VmgsResource::ReprovisionSingle(convert_disk(disk).await?)
571 }
572 PetriVmgsResource::Ephemeral => VmgsResource::Ephemeral,
573 })
574}
575
576async fn petri_disk_to_openvmm(disk: &Disk) -> anyhow::Result<Resource<DiskHandleKind>> {
577 Ok(match disk {
578 Disk::Memory(len) => LayeredDiskHandle::single_layer(RamDiskLayerHandle {
579 len: Some(*len),
580 sector_size: None,
581 })
582 .into_resource(),
583 Disk::Differencing(DiskPath::Local(path)) => memdiff_disk(path).await?,
584 Disk::Differencing(DiskPath::Remote { url }) => memdiff_remote_disk(url)?,
585 Disk::SqliteDifferencing {
586 base: DiskPath::Local(path),
587 dbhd_path,
588 } => sqldiff_disk(path, dbhd_path).await?,
589 Disk::SqliteDifferencing {
590 base: DiskPath::Remote { url },
591 dbhd_path,
592 } => sqldiff_remote_disk(url, dbhd_path)?,
593 Disk::Persistent(path) => {
594 open_disk_type(
595 path.as_ref(),
596 OpenDiskOptions {
597 read_only: false,
598 direct: false,
599 },
600 )
601 .await?
602 }
603 Disk::Temporary(path) => {
604 open_disk_type(
605 path.as_ref(),
606 OpenDiskOptions {
607 read_only: false,
608 direct: false,
609 },
610 )
611 .await?
612 }
613 })
614}