1use super::NIC_MAC_ADDRESS;
7use super::OpenVmmPetriBackend;
8use crate::BootDeviceType;
9use crate::Firmware;
10use crate::PetriVmBuilder;
11use petri_artifacts_common::tags::MachineArch;
12
13impl PetriVmBuilder<OpenVmmPetriBackend> {
14 pub fn with_mshv_platform(self) -> Self {
21 assert!(
22 self.arch() == MachineArch::X86_64
23 && MachineArch::host() == MachineArch::X86_64
24 && cfg!(target_os = "linux"),
25 "mshv platform requires x86-64 Linux"
26 );
27 assert!(
28 matches!(self.config.firmware, Firmware::Uefi { .. }),
29 "mshv platform requires UEFI firmware"
30 );
31 self.with_no_vmbus()
32 .with_virtio_vsock(true)
33 .with_virtio_console()
34 .with_nested_virt()
35 .with_serial_output()
36 .with_boot_device_type(BootDeviceType::PcieVirtioBlk)
37 .with_pcie_boot_port("s1rc0rp0")
38 .modify_backend(|backend| {
39 backend
40 .with_pcie_root_topology_at(1, 1, 1, 2)
41 .with_virtio_nic("s1rc0rp1", NIC_MAC_ADDRESS)
42 })
43 }
44}
45
46#[cfg(test)]
47mod tests {
48 use super::super::PetriVmConfigOpenVmm;
49 use super::*;
50 use crate::Disk;
51 use crate::PetriLogSource;
52 use crate::PetriVmArtifacts;
53 use crate::ResolvedArtifact;
54 use crate::UefiConfig;
55 use crate::UefiGuest;
56 use crate::mshv_boot::MshvBoot;
57 use petri_artifacts_common::tags::OsFlavor;
58 use test_with_tracing::test;
59 use vm_resource::IntoResource;
60
61 fn builder_with_guest(
62 driver: &pal_async::DefaultDriver,
63 logs: &PetriLogSource,
64 firmware: &std::path::Path,
65 guest: UefiGuest,
66 arch: MachineArch,
67 ) -> anyhow::Result<PetriVmBuilder<OpenVmmPetriBackend>> {
68 Ok(PetriVmBuilder::minimal(
69 "platform",
70 logs,
71 PetriVmArtifacts {
72 backend: OpenVmmPetriBackend::from_binary("unused-openvmm".into()),
73 firmware: Firmware::Uefi {
74 uefi_firmware: ResolvedArtifact::from_local_path(firmware.to_path_buf()),
75 guest,
76 uefi_config: UefiConfig {
77 disable_frontpage: false,
78 ..Default::default()
79 },
80 },
81 arch,
82 agent_image: None,
83 openhcl_agent_image: None,
84 pipette_binary: None,
85 nested_test: None,
86 },
87 driver,
88 )?
89 .with_external_pipette())
90 }
91
92 fn mshv_guest(dir: &std::path::Path) -> anyhow::Result<UefiGuest> {
93 let boot = dir.join("boot");
94 fs_err::create_dir_all(boot.join("Windows/System32"))?;
95 for name in [
96 "HvLoader.efi",
97 "lxhvloader.dll",
98 "Windows/System32/hvix64.exe",
99 "Windows/System32/hvax64.exe",
100 "mshv_host_boot.efi",
101 ] {
102 fs_err::write(boot.join(name), name)?;
103 }
104 let kernel = dir.join("kernel");
105 fs_err::write(&kernel, b"root kernel")?;
106 let initrd = tempfile::NamedTempFile::new_in(dir)?;
107 fs_err::write(initrd.path(), b"prepared initrd")?;
108 Ok(UefiGuest::Mshv(MshvBoot {
109 host_boot: ResolvedArtifact::from_local_path(boot.join("mshv_host_boot.efi")),
110 kernel: ResolvedArtifact::from_local_path(kernel),
111 initrd: crate::PetriInitrd {
112 path: crate::TempOrPersistentPath::Temp(std::sync::Arc::new(
113 initrd.into_temp_path(),
114 )),
115 rdinit_param: "/caller-init".into(),
116 },
117 boot_artifacts: ResolvedArtifact::from_local_path(dir.to_path_buf()),
118 kernel_cmdline: "console=ttyS0 panic=-1".into(),
119 hypervisor_cmdline: "MSHV_ENABLE=TRUE".into(),
120 }))
121 }
122
123 #[test]
124 fn mshv_guest_prepares_an_owned_boot_disk() -> anyhow::Result<()> {
125 let temp = tempfile::tempdir()?;
126 let logs = PetriLogSource::file_only(temp.path())?;
127 pal_async::DefaultPool::run_with(async |driver| {
128 let guest = mshv_guest(temp.path())?;
129 let UefiGuest::Mshv(boot) = &guest else {
130 unreachable!()
131 };
132 let initrd = boot.initrd.path.as_ref().to_path_buf();
133 let builder = builder_with_guest(
134 &driver,
135 &logs,
136 &temp.path().join("unused-firmware"),
137 guest,
138 MachineArch::X86_64,
139 )?
140 .with_boot_device_type(BootDeviceType::PcieVirtioBlk)
141 .with_pcie_boot_port("s1rc0rp0");
142 assert!(builder.config.pcie_virtio_blk_drives.is_empty());
143 assert_eq!(builder.properties().os_flavor, OsFlavor::Linux);
144 assert!(!builder.properties().is_linux_direct);
145 let builder = builder.add_boot_disk()?;
146 let [drive] = builder.config.pcie_virtio_blk_drives.as_slice() else {
147 panic!("expected one mshv boot disk")
148 };
149 assert_eq!(drive.port_name, "s1rc0rp0");
150 let Some(Disk::Temporary(path)) = &drive.drive.disk else {
151 panic!("expected an owned temporary disk")
152 };
153 let output = path.to_path_buf();
154 assert!(output.exists());
155 assert!(initrd.exists());
156 drop(builder);
157 assert!(!output.exists());
158 assert!(!initrd.exists());
159 Ok(())
160 })
161 }
162
163 #[test]
164 fn mshv_guest_rejects_unsupported_firmware_and_reports_missing_inputs() -> anyhow::Result<()> {
165 let temp = tempfile::tempdir()?;
166 let logs = PetriLogSource::file_only(temp.path())?;
167 pal_async::DefaultPool::run_with(async |driver| {
168 let error = builder_with_guest(
169 &driver,
170 &logs,
171 &temp.path().join("unused-firmware"),
172 mshv_guest(temp.path())?,
173 MachineArch::Aarch64,
174 )
175 .expect_err("mshv requires x86-64");
176 assert!(error.to_string().contains("x86-64 UEFI"), "{error:#}");
177 let error = PetriVmBuilder::minimal(
178 "unsupported",
179 &logs,
180 PetriVmArtifacts {
181 backend: OpenVmmPetriBackend::from_binary("unused-openvmm".into()),
182 firmware: Firmware::OpenhclUefi {
183 guest: mshv_guest(temp.path())?,
184 igvm_firmware: crate::IgvmFirmwareSource::VmgsOrInBox,
185 isolation: None,
186 uefi_config: Default::default(),
187 openhcl_config: Default::default(),
188 },
189 arch: MachineArch::X86_64,
190 agent_image: None,
191 openhcl_agent_image: None,
192 pipette_binary: None,
193 nested_test: None,
194 },
195 &driver,
196 )
197 .expect_err("mshv with OpenHCL is unsupported");
198 assert!(error.to_string().contains("OpenHCL"), "{error:#}");
199 let builder = builder_with_guest(
200 &driver,
201 &logs,
202 &temp.path().join("unused-firmware"),
203 mshv_guest(temp.path())?,
204 MachineArch::X86_64,
205 )?;
206 fs_err::remove_file(temp.path().join("kernel"))?;
207 let error = builder.add_boot_disk().expect_err("missing kernel");
208 assert!(
209 error.to_string().contains("composing mshv boot disk"),
210 "{error:#}"
211 );
212 assert!(format!("{error:#}").contains("kernel"), "{error:#}");
213 Ok(())
214 })
215 }
216
217 async fn configure(
218 builder: PetriVmBuilder<OpenVmmPetriBackend>,
219 ) -> anyhow::Result<PetriVmConfigOpenVmm> {
220 let mut builder = builder.add_boot_disk()?.add_agent_disks()?;
221 let properties = builder.properties();
222 let modify = builder.modify_vmm_config.take();
223 let config = PetriVmConfigOpenVmm::new(
224 &ResolvedArtifact::from_local_path("unused-openvmm".into()),
225 builder.config,
226 &builder.resources,
227 &properties,
228 )
229 .await?;
230 let config = match modify {
231 Some(modify) => modify.0(config),
232 None => config,
233 };
234 Ok(config.with_private_virtio_devices())
235 }
236
237 #[test]
238 fn linux_direct_boot_respects_agent_ownership() -> anyhow::Result<()> {
239 let temp = tempfile::tempdir()?;
240 let logs = PetriLogSource::file_only(temp.path())?;
241 let base = temp.path().join("base");
242 let prepared = temp.path().join("prepared");
243 fs_err::write(&base, b"base initrd")?;
244 fs_err::write(&prepared, b"prepared initrd")?;
245 pal_async::DefaultPool::run_with(async |driver| {
246 for (arch, external, prebuilt) in [
248 (MachineArch::X86_64, false, false),
249 (MachineArch::X86_64, false, true),
250 (MachineArch::X86_64, true, false),
251 (MachineArch::X86_64, true, true),
252 (MachineArch::Aarch64, false, true),
253 (MachineArch::Aarch64, true, false),
254 (MachineArch::Aarch64, true, true),
255 ] {
256 let mut builder = PetriVmBuilder::minimal(
257 "direct",
258 &logs,
259 PetriVmArtifacts {
260 backend: OpenVmmPetriBackend::from_binary("unused-openvmm".into()),
261 firmware: Firmware::LinuxDirect {
262 kernel: ResolvedArtifact::from_local_path(base.clone()),
263 initrd: ResolvedArtifact::from_local_path(base.clone()),
264 },
265 arch,
266 agent_image: None,
267 openhcl_agent_image: None,
268 pipette_binary: None,
269 nested_test: None,
270 },
271 &driver,
272 )?
273 .with_serial_output();
274 if external {
275 builder = builder.with_external_pipette();
276 }
277 if prebuilt {
278 builder = builder.with_prebuilt_initrd(crate::PetriInitrd {
279 path: crate::TempOrPersistentPath::Persistent(prepared.clone()),
280 rdinit_param: "/external-init".into(),
281 });
282 }
283 let backend = configure(builder).await?;
284 assert_eq!(
285 backend.resources.linux_direct_serial_agent.is_some(),
286 !external && !prebuilt
287 );
288 let openvmm_defs::config::LoadMode::Linux {
289 initrd: Some(mut file),
290 cmdline,
291 ..
292 } = backend.config.load_mode
293 else {
294 panic!("expected Linux direct")
295 };
296 if prebuilt {
297 assert!(cmdline.contains("rdinit=/external-init"));
298 } else if external {
299 assert!(
300 !cmdline
301 .split_whitespace()
302 .any(|arg| arg.starts_with("rdinit="))
303 );
304 } else {
305 assert!(cmdline.contains("rdinit=/bin/sh"));
306 }
307 let mut contents = String::new();
308 std::io::Read::read_to_string(&mut file, &mut contents)?;
309 assert_eq!(
310 contents,
311 if prebuilt {
312 "prepared initrd"
313 } else {
314 "base initrd"
315 }
316 );
317 }
318 Ok(())
319 })
320 }
321
322 #[cfg(any(windows, target_os = "linux"))]
323 #[test]
324 fn shares_get_private_ports_after_all_backend_modifiers() -> anyhow::Result<()> {
325 let temp = tempfile::tempdir()?;
326 let logs = PetriLogSource::file_only(temp.path())?;
327 let input = temp.path().join("boot-input");
328 fs_err::write(&input, b"unused boot input")?;
329 pal_async::DefaultPool::run_with(async |driver| {
330 for custom_topology in [false, true] {
331 let mut builder = PetriVmBuilder::minimal(
332 "shares",
333 &logs,
334 PetriVmArtifacts {
335 backend: OpenVmmPetriBackend::from_binary("unused-openvmm".into()),
336 firmware: Firmware::LinuxDirect {
337 kernel: ResolvedArtifact::from_local_path(input.clone()),
338 initrd: ResolvedArtifact::from_local_path(input.clone()),
339 },
340 arch: MachineArch::host(),
341 agent_image: None,
342 openhcl_agent_image: None,
343 pipette_binary: None,
344 nested_test: None,
345 },
346 &driver,
347 )?
348 .with_shared_directory("z-data", temp.path(), "/data")
349 .with_shared_directory("a-cache", temp.path(), "/cache");
350 if custom_topology {
351 builder = builder.modify_backend(|b| {
352 b.with_pcie_root_topology_at(1, 1, 1, 1)
353 .with_custom_config(|c| {
354 c.pcie_root_complexes[0].ports[0].name = "petri-share-0".into();
355 c.pcie_devices.push(openvmm_defs::config::PcieDeviceConfig {
356 port_name: "petri-share-0".into(),
357 resource: virtio_resources::VirtioPciDeviceHandle(
358 virtio_resources::rng::VirtioRngHandle.into_resource(),
359 )
360 .into_resource(),
361 });
362 })
363 });
364 }
365 let backend = configure(builder).await?;
366 let [root] = backend.config.pcie_root_complexes.as_slice() else {
367 panic!("expected one root complex")
368 };
369 assert_eq!(root.segment, u16::from(custom_topology));
370 let first_share = usize::from(custom_topology);
371 for i in 0..2 {
372 let device = &backend.config.pcie_devices[first_share + i];
373 assert_eq!(device.port_name, format!("petri-share-{}", first_share + i));
374 assert!(root.ports.iter().any(|p| p.name == device.port_name));
375 }
376 assert_eq!(root.ports.len(), first_share + 2);
377 assert!(backend.guest_shares.is_empty());
378 }
379 Ok(())
380 })
381 }
382
383 #[test]
384 fn virtio_console_keeps_external_linux_direct_initrd_and_uses_vsock() -> anyhow::Result<()> {
385 let temp = tempfile::tempdir()?;
386 let logs = PetriLogSource::file_only(temp.path())?;
387 let base = temp.path().join("base-initrd");
388 fs_err::write(&base, b"external guest bootstrap")?;
389 pal_async::DefaultPool::run_with(async |driver| {
390 let builder = PetriVmBuilder::minimal(
391 "virtio-console",
392 &logs,
393 PetriVmArtifacts {
394 backend: OpenVmmPetriBackend::from_binary("unused-openvmm".into()),
395 firmware: Firmware::LinuxDirect {
396 kernel: ResolvedArtifact::from_local_path(base.clone()),
397 initrd: ResolvedArtifact::from_local_path(base.clone()),
398 },
399 arch: MachineArch::X86_64,
400 agent_image: None,
401 openhcl_agent_image: None,
402 pipette_binary: None,
403 nested_test: None,
404 },
405 &driver,
406 )?
407 .with_external_pipette()
408 .with_virtio_console()
409 .with_no_vmbus();
410 let mut backend = configure(builder).await?;
411 assert!(backend.resources.linux_direct_serial_agent.is_none());
412 assert!(backend.resources.tcp_pipette_port.is_none());
413 assert!(backend.config.vmbus.is_none());
414 assert_eq!(backend.config.pcie_devices.len(), 2);
415 let openvmm_defs::config::LoadMode::Linux {
416 cmdline,
417 initrd: Some(ref mut file),
418 ..
419 } = backend.config.load_mode
420 else {
421 panic!("expected Linux direct")
422 };
423 assert!(cmdline.contains("console=hvc0"));
424 assert!(!cmdline.contains("console=ttyS0"));
425 assert!(cmdline.contains("initcall_blacklist=hv_sock_init"));
426 assert!(!cmdline.contains("rdinit="));
427 let mut bytes = Vec::new();
428 std::io::Read::read_to_end(file, &mut bytes)?;
429 assert_eq!(bytes, b"external guest bootstrap");
430 Ok(())
431 })
432 }
433
434 #[test]
435 fn isolated_agent_is_external_only_when_requested() -> anyhow::Result<()> {
436 let temp = tempfile::tempdir()?;
437 let logs = PetriLogSource::file_only(temp.path())?;
438 let igvm = temp.path().join("guest.igvm");
439 fs_err::write(&igvm, b"unused measured image")?;
440 pal_async::DefaultPool::run_with(async |driver| {
441 for requested in [false, true] {
442 let builder = PetriVmBuilder::minimal(
443 "isolated",
444 &logs,
445 PetriVmArtifacts {
446 backend: OpenVmmPetriBackend::from_binary("unused-openvmm".into()),
447 firmware: Firmware::IgvmLinuxDirect {
448 igvm_path: ResolvedArtifact::from_local_path(igvm.clone()),
449 isolation: crate::IsolationType::Snp,
450 },
451 arch: MachineArch::X86_64,
452 agent_image: None,
453 openhcl_agent_image: None,
454 pipette_binary: requested
455 .then(|| ResolvedArtifact::from_local_path("unused-pipette".into())),
456 nested_test: None,
457 },
458 &driver,
459 )?
460 .with_serial_output();
461 assert_eq!(builder.using_vtl0_pipette(), requested);
462 assert!(!builder.properties().uses_pipette_as_init);
463 assert!(builder.pipette_binary.is_none());
464 let backend = configure(builder).await?;
465 assert!(backend.resources.linux_direct_serial_agent.is_none());
466 assert!(matches!(
467 backend.config.load_mode,
468 openvmm_defs::config::LoadMode::Igvm { .. }
469 ));
470 }
471 Ok(())
472 })
473 }
474
475 #[test]
476 fn platform_composes_without_replacing_uefi_or_other_roots() -> anyhow::Result<()> {
477 if MachineArch::host() != MachineArch::X86_64 || !cfg!(target_os = "linux") {
478 return Ok(());
479 }
480 let temp = tempfile::tempdir()?;
481 let firmware = tempfile::NamedTempFile::new()?;
482 let logs = PetriLogSource::file_only(temp.path())?;
483 pal_async::DefaultPool::run_with(async |driver| {
484 let builder = builder_with_guest(
485 &driver,
486 &logs,
487 firmware.path(),
488 mshv_guest(temp.path())?,
489 MachineArch::X86_64,
490 )?
491 .modify_backend(|b| b.with_pcie_root_topology(1, 1, 1))
492 .with_mshv_platform()
493 .with_shared_directory("data", temp.path(), "/mounted elsewhere")
494 .modify_backend(|b| {
495 b.with_pcie_root_topology(1, 1, 2).with_custom_config(|c| {
496 let root = c.pcie_root_complexes.last_mut().unwrap();
497 for (port, name) in root
498 .ports
499 .iter_mut()
500 .zip(["petri-console-0", "petri-vsock-0"])
501 {
502 port.name = name.into();
503 c.pcie_devices.push(openvmm_defs::config::PcieDeviceConfig {
504 port_name: name.into(),
505 resource: virtio_resources::VirtioPciDeviceHandle(
506 virtio_resources::rng::VirtioRngHandle.into_resource(),
507 )
508 .into_resource(),
509 });
510 }
511 })
512 });
513 assert!(matches!(
514 builder.config.firmware,
515 Firmware::Uefi {
516 guest: UefiGuest::Mshv(_),
517 uefi_config: UefiConfig {
518 disable_frontpage: false,
519 ..
520 },
521 ..
522 }
523 ));
524 assert!(!builder.properties().is_linux_direct);
525 assert!(builder.properties().no_vmbus);
526 assert!(builder.properties().use_virtio_vsock);
527 assert!(builder.properties().use_virtio_console);
528 let backend = configure(builder).await?;
529 assert_eq!(
530 backend
531 .config
532 .pcie_root_complexes
533 .iter()
534 .map(|rc| rc.segment)
535 .collect::<Vec<_>>(),
536 [0, 1, 2]
537 );
538 assert_eq!(
539 backend.config.pcie_root_complexes[0].ports[0].name,
540 "s0rc0rp0"
541 );
542 assert_eq!(backend.config.pcie_root_complexes[1].name, "s1rc0");
543 assert_eq!(backend.config.pcie_root_complexes[1].ports.len(), 5);
544 assert!(backend.resources.tcp_pipette_port.is_none());
545 for port in [
546 "s1rc0rp0",
547 "s1rc0rp1",
548 "petri-console-1",
549 "petri-vsock-1",
550 "petri-share-0",
551 ] {
552 assert!(
553 backend
554 .config
555 .pcie_devices
556 .iter()
557 .any(|d| d.port_name == port)
558 );
559 assert!(
560 backend.config.pcie_root_complexes[1]
561 .ports
562 .iter()
563 .any(|p| p.name == port)
564 );
565 }
566 assert!(backend.guest_shares.is_empty());
567 Ok(())
568 })
569 }
570
571 #[test]
572 #[should_panic(expected = "PCI segment range overlaps an existing root complex")]
573 fn segment_conflicts_stop_later_modifiers() {
574 let temp = tempfile::tempdir().unwrap();
575 let firmware = tempfile::NamedTempFile::new().unwrap();
576 let logs = PetriLogSource::file_only(temp.path()).unwrap();
577 pal_async::DefaultPool::run_with(async |driver| {
578 let builder = builder_with_guest(
579 &driver,
580 &logs,
581 firmware.path(),
582 UefiGuest::None,
583 MachineArch::host(),
584 )?
585 .modify_backend(|b| b.with_pcie_root_topology_at(1, 1, 1, 1))
586 .modify_backend(|b| b.with_pcie_root_topology_at(1, 1, 1, 1))
587 .modify_backend(|_| panic!("segment conflict should stop later modifiers"));
588 configure(builder).await?;
589 Ok::<_, anyhow::Error>(())
590 })
591 .unwrap();
592 }
593}