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(false)
33 .with_nested_virt()
34 .with_serial_output()
35 .with_boot_device_type(BootDeviceType::PcieVirtioBlk)
36 .with_pcie_boot_port("s1rc0rp0")
37 .modify_backend(|backend| {
38 backend
39 .with_pcie_root_topology_at(1, 1, 1, 2)
40 .with_tcp_pipette_nic("s1rc0rp1", NIC_MAC_ADDRESS)
41 })
42 }
43}
44
45#[cfg(test)]
46mod tests {
47 use super::super::PetriVmConfigOpenVmm;
48 use super::*;
49 use crate::Disk;
50 use crate::PetriLogSource;
51 use crate::PetriVmArtifacts;
52 use crate::ResolvedArtifact;
53 use crate::UefiConfig;
54 use crate::UefiGuest;
55 use crate::mshv_boot::MshvBoot;
56 use petri_artifacts_common::tags::OsFlavor;
57 use test_with_tracing::test;
58 #[cfg(any(windows, target_os = "linux"))]
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 fs_err::create_dir_all(boot.join("EFI/BOOT"))?;
96 for name in [
97 "HvLoader.efi",
98 "lxhvloader.dll",
99 "Windows/System32/hvix64.exe",
100 "Windows/System32/hvax64.exe",
101 "EFI/BOOT/bootx64.efi",
102 "EFI/BOOT/grubx64.efi",
103 ] {
104 fs_err::write(boot.join(name), name)?;
105 }
106 let kernel = dir.join("kernel");
107 fs_err::write(&kernel, b"root kernel")?;
108 let initrd = tempfile::NamedTempFile::new_in(dir)?;
109 fs_err::write(initrd.path(), b"prepared initrd")?;
110 Ok(UefiGuest::Mshv(MshvBoot {
111 kernel: ResolvedArtifact::from_local_path(kernel),
112 initrd: crate::PetriInitrd {
113 path: crate::TempOrPersistentPath::Temp(std::sync::Arc::new(
114 initrd.into_temp_path(),
115 )),
116 rdinit_param: "/caller-init".into(),
117 },
118 boot_artifacts: ResolvedArtifact::from_local_path(dir.to_path_buf()),
119 }))
120 }
121
122 #[test]
123 fn mshv_guest_prepares_an_owned_boot_disk() -> anyhow::Result<()> {
124 let temp = tempfile::tempdir()?;
125 let logs = PetriLogSource::file_only(temp.path())?;
126 pal_async::DefaultPool::run_with(async |driver| {
127 let guest = mshv_guest(temp.path())?;
128 let UefiGuest::Mshv(boot) = &guest else {
129 unreachable!()
130 };
131 let initrd = boot.initrd.path.as_ref().to_path_buf();
132 let builder = builder_with_guest(
133 &driver,
134 &logs,
135 &temp.path().join("unused-firmware"),
136 guest,
137 MachineArch::X86_64,
138 )?
139 .with_boot_device_type(BootDeviceType::PcieVirtioBlk)
140 .with_pcie_boot_port("s1rc0rp0");
141 assert!(builder.config.pcie_virtio_blk_drives.is_empty());
142 assert_eq!(builder.properties().os_flavor, OsFlavor::Linux);
143 assert!(!builder.properties().is_linux_direct);
144 let builder = builder.add_boot_disk()?;
145 let [drive] = builder.config.pcie_virtio_blk_drives.as_slice() else {
146 panic!("expected one mshv boot disk")
147 };
148 assert_eq!(drive.port_name, "s1rc0rp0");
149 let Some(Disk::Temporary(path)) = &drive.drive.disk else {
150 panic!("expected an owned temporary disk")
151 };
152 let output = path.to_path_buf();
153 assert!(output.exists());
154 assert!(initrd.exists());
155 drop(builder);
156 assert!(!output.exists());
157 assert!(!initrd.exists());
158 Ok(())
159 })
160 }
161
162 #[test]
163 fn mshv_guest_rejects_unsupported_firmware_and_reports_missing_inputs() -> anyhow::Result<()> {
164 let temp = tempfile::tempdir()?;
165 let logs = PetriLogSource::file_only(temp.path())?;
166 pal_async::DefaultPool::run_with(async |driver| {
167 let error = builder_with_guest(
168 &driver,
169 &logs,
170 &temp.path().join("unused-firmware"),
171 mshv_guest(temp.path())?,
172 MachineArch::Aarch64,
173 )
174 .expect_err("mshv requires x86-64");
175 assert!(error.to_string().contains("x86-64 UEFI"), "{error:#}");
176 let error = PetriVmBuilder::minimal(
177 "unsupported",
178 &logs,
179 PetriVmArtifacts {
180 backend: OpenVmmPetriBackend::from_binary("unused-openvmm".into()),
181 firmware: Firmware::OpenhclUefi {
182 guest: mshv_guest(temp.path())?,
183 igvm_firmware: crate::IgvmFirmwareSource::VmgsOrInBox,
184 isolation: None,
185 uefi_config: Default::default(),
186 openhcl_config: Default::default(),
187 },
188 arch: MachineArch::X86_64,
189 agent_image: None,
190 openhcl_agent_image: None,
191 pipette_binary: None,
192 nested_test: None,
193 },
194 &driver,
195 )
196 .expect_err("mshv with OpenHCL is unsupported");
197 assert!(error.to_string().contains("OpenHCL"), "{error:#}");
198 let builder = builder_with_guest(
199 &driver,
200 &logs,
201 &temp.path().join("unused-firmware"),
202 mshv_guest(temp.path())?,
203 MachineArch::X86_64,
204 )?;
205 fs_err::remove_file(temp.path().join("kernel"))?;
206 let error = builder.add_boot_disk().expect_err("missing kernel");
207 assert!(
208 error.to_string().contains("composing mshv boot disk"),
209 "{error:#}"
210 );
211 assert!(format!("{error:#}").contains("kernel"), "{error:#}");
212 Ok(())
213 })
214 }
215
216 async fn configure(
217 builder: PetriVmBuilder<OpenVmmPetriBackend>,
218 ) -> anyhow::Result<PetriVmConfigOpenVmm> {
219 let mut builder = builder.add_boot_disk()?.add_agent_disks()?;
220 let properties = builder.properties();
221 let modify = builder.modify_vmm_config.take();
222 let config = PetriVmConfigOpenVmm::new(
223 &ResolvedArtifact::from_local_path("unused-openvmm".into()),
224 builder.config,
225 &builder.resources,
226 &properties,
227 )
228 .await?;
229 let config = match modify {
230 Some(modify) => modify.0(config),
231 None => config,
232 };
233 Ok(config.with_guest_shares())
234 }
235
236 #[test]
237 fn linux_direct_boot_respects_agent_ownership() -> anyhow::Result<()> {
238 let temp = tempfile::tempdir()?;
239 let logs = PetriLogSource::file_only(temp.path())?;
240 let base = temp.path().join("base");
241 let prepared = temp.path().join("prepared");
242 fs_err::write(&base, b"base initrd")?;
243 fs_err::write(&prepared, b"prepared initrd")?;
244 pal_async::DefaultPool::run_with(async |driver| {
245 for (arch, external, prebuilt) in [
247 (MachineArch::X86_64, false, false),
248 (MachineArch::X86_64, false, true),
249 (MachineArch::X86_64, true, false),
250 (MachineArch::X86_64, true, true),
251 (MachineArch::Aarch64, false, true),
252 (MachineArch::Aarch64, true, false),
253 (MachineArch::Aarch64, true, true),
254 ] {
255 let mut builder = PetriVmBuilder::minimal(
256 "direct",
257 &logs,
258 PetriVmArtifacts {
259 backend: OpenVmmPetriBackend::from_binary("unused-openvmm".into()),
260 firmware: Firmware::LinuxDirect {
261 kernel: ResolvedArtifact::from_local_path(base.clone()),
262 initrd: ResolvedArtifact::from_local_path(base.clone()),
263 },
264 arch,
265 agent_image: None,
266 openhcl_agent_image: None,
267 pipette_binary: None,
268 nested_test: None,
269 },
270 &driver,
271 )?
272 .with_serial_output();
273 if external {
274 builder = builder.with_external_pipette();
275 }
276 if prebuilt {
277 builder = builder.with_prebuilt_initrd(crate::PetriInitrd {
278 path: crate::TempOrPersistentPath::Persistent(prepared.clone()),
279 rdinit_param: "/external-init".into(),
280 });
281 }
282 let backend = configure(builder).await?;
283 assert_eq!(
284 backend.resources.linux_direct_serial_agent.is_some(),
285 !external && !prebuilt
286 );
287 let openvmm_defs::config::LoadMode::Linux {
288 initrd: Some(mut file),
289 cmdline,
290 ..
291 } = backend.config.load_mode
292 else {
293 panic!("expected Linux direct")
294 };
295 if prebuilt {
296 assert!(cmdline.contains("rdinit=/external-init"));
297 } else if external {
298 assert!(
299 !cmdline
300 .split_whitespace()
301 .any(|arg| arg.starts_with("rdinit="))
302 );
303 } else {
304 assert!(cmdline.contains("rdinit=/bin/sh"));
305 }
306 let mut contents = String::new();
307 std::io::Read::read_to_string(&mut file, &mut contents)?;
308 assert_eq!(
309 contents,
310 if prebuilt {
311 "prepared initrd"
312 } else {
313 "base initrd"
314 }
315 );
316 }
317 Ok(())
318 })
319 }
320
321 #[cfg(any(windows, target_os = "linux"))]
322 #[test]
323 fn shares_get_private_ports_after_all_backend_modifiers() -> anyhow::Result<()> {
324 let temp = tempfile::tempdir()?;
325 let logs = PetriLogSource::file_only(temp.path())?;
326 let input = temp.path().join("boot-input");
327 fs_err::write(&input, b"unused boot input")?;
328 pal_async::DefaultPool::run_with(async |driver| {
329 for custom_topology in [false, true] {
330 let mut builder = PetriVmBuilder::minimal(
331 "shares",
332 &logs,
333 PetriVmArtifacts {
334 backend: OpenVmmPetriBackend::from_binary("unused-openvmm".into()),
335 firmware: Firmware::LinuxDirect {
336 kernel: ResolvedArtifact::from_local_path(input.clone()),
337 initrd: ResolvedArtifact::from_local_path(input.clone()),
338 },
339 arch: MachineArch::host(),
340 agent_image: None,
341 openhcl_agent_image: None,
342 pipette_binary: None,
343 nested_test: None,
344 },
345 &driver,
346 )?
347 .with_shared_directory("z-data", temp.path(), "/data")
348 .with_shared_directory("a-cache", temp.path(), "/cache");
349 if custom_topology {
350 builder = builder.modify_backend(|b| {
351 b.with_pcie_root_topology_at(1, 1, 1, 1)
352 .with_custom_config(|c| {
353 c.pcie_root_complexes[0].ports[0].name = "petri-share-0".into();
354 c.pcie_devices.push(openvmm_defs::config::PcieDeviceConfig {
355 port_name: "petri-share-0".into(),
356 resource: virtio_resources::VirtioPciDeviceHandle(
357 virtio_resources::rng::VirtioRngHandle.into_resource(),
358 )
359 .into_resource(),
360 });
361 })
362 });
363 }
364 let backend = configure(builder).await?;
365 let [root] = backend.config.pcie_root_complexes.as_slice() else {
366 panic!("expected one root complex")
367 };
368 assert_eq!(root.segment, u16::from(custom_topology));
369 let first_share = usize::from(custom_topology);
370 for i in 0..2 {
371 let device = &backend.config.pcie_devices[first_share + i];
372 assert_eq!(device.port_name, format!("petri-share-{}", first_share + i));
373 assert!(root.ports.iter().any(|p| p.name == device.port_name));
374 }
375 assert_eq!(root.ports.len(), first_share + 2);
376 assert!(backend.guest_shares.is_empty());
377 }
378 Ok(())
379 })
380 }
381
382 #[test]
383 fn isolated_agent_is_external_only_when_requested() -> anyhow::Result<()> {
384 let temp = tempfile::tempdir()?;
385 let logs = PetriLogSource::file_only(temp.path())?;
386 let igvm = temp.path().join("guest.igvm");
387 fs_err::write(&igvm, b"unused measured image")?;
388 pal_async::DefaultPool::run_with(async |driver| {
389 for requested in [false, true] {
390 let builder = PetriVmBuilder::minimal(
391 "isolated",
392 &logs,
393 PetriVmArtifacts {
394 backend: OpenVmmPetriBackend::from_binary("unused-openvmm".into()),
395 firmware: Firmware::IgvmLinuxDirect {
396 igvm_path: ResolvedArtifact::from_local_path(igvm.clone()),
397 isolation: crate::IsolationType::Snp,
398 },
399 arch: MachineArch::X86_64,
400 agent_image: None,
401 openhcl_agent_image: None,
402 pipette_binary: requested
403 .then(|| ResolvedArtifact::from_local_path("unused-pipette".into())),
404 nested_test: None,
405 },
406 &driver,
407 )?
408 .with_serial_output();
409 assert_eq!(builder.using_vtl0_pipette(), requested);
410 assert!(!builder.properties().uses_pipette_as_init);
411 assert!(builder.pipette_binary.is_none());
412 let backend = configure(builder).await?;
413 assert!(backend.resources.linux_direct_serial_agent.is_none());
414 assert!(matches!(
415 backend.config.load_mode,
416 openvmm_defs::config::LoadMode::Igvm { .. }
417 ));
418 }
419 Ok(())
420 })
421 }
422
423 #[test]
424 fn platform_composes_without_replacing_uefi_or_other_roots() -> anyhow::Result<()> {
425 if MachineArch::host() != MachineArch::X86_64 || !cfg!(target_os = "linux") {
426 return Ok(());
427 }
428 let temp = tempfile::tempdir()?;
429 let firmware = tempfile::NamedTempFile::new()?;
430 let logs = PetriLogSource::file_only(temp.path())?;
431 pal_async::DefaultPool::run_with(async |driver| {
432 let builder = builder_with_guest(
433 &driver,
434 &logs,
435 firmware.path(),
436 mshv_guest(temp.path())?,
437 MachineArch::X86_64,
438 )?
439 .modify_backend(|b| b.with_pcie_root_topology(1, 1, 1))
440 .with_mshv_platform()
441 .with_shared_directory("data", temp.path(), "/mounted elsewhere")
442 .modify_backend(|b| b.with_pcie_root_topology(1, 1, 1));
443 assert!(matches!(
444 builder.config.firmware,
445 Firmware::Uefi {
446 guest: UefiGuest::Mshv(_),
447 uefi_config: UefiConfig {
448 disable_frontpage: false,
449 ..
450 },
451 ..
452 }
453 ));
454 assert!(!builder.properties().is_linux_direct);
455 assert!(builder.properties().no_vmbus);
456 assert!(!builder.properties().use_virtio_vsock);
457 let backend = configure(builder).await?;
458 assert_eq!(
459 backend
460 .config
461 .pcie_root_complexes
462 .iter()
463 .map(|rc| rc.segment)
464 .collect::<Vec<_>>(),
465 [0, 1, 2]
466 );
467 assert_eq!(
468 backend.config.pcie_root_complexes[0].ports[0].name,
469 "s0rc0rp0"
470 );
471 assert_eq!(backend.config.pcie_root_complexes[1].name, "s1rc0");
472 assert_eq!(backend.config.pcie_root_complexes[1].ports.len(), 3);
473 for port in ["s1rc0rp0", "s1rc0rp1", "petri-share-0"] {
474 assert!(
475 backend
476 .config
477 .pcie_devices
478 .iter()
479 .any(|d| d.port_name == port)
480 );
481 }
482 assert!(backend.guest_shares.is_empty());
483 Ok(())
484 })
485 }
486
487 #[test]
488 #[should_panic(expected = "PCI segment range overlaps an existing root complex")]
489 fn segment_conflicts_stop_later_modifiers() {
490 let temp = tempfile::tempdir().unwrap();
491 let firmware = tempfile::NamedTempFile::new().unwrap();
492 let logs = PetriLogSource::file_only(temp.path()).unwrap();
493 pal_async::DefaultPool::run_with(async |driver| {
494 let builder = builder_with_guest(
495 &driver,
496 &logs,
497 firmware.path(),
498 UefiGuest::None,
499 MachineArch::host(),
500 )?
501 .modify_backend(|b| b.with_pcie_root_topology_at(1, 1, 1, 1))
502 .modify_backend(|b| b.with_pcie_root_topology_at(1, 1, 1, 1))
503 .modify_backend(|_| panic!("segment conflict should stop later modifiers"));
504 configure(builder).await?;
505 Ok::<_, anyhow::Error>(())
506 })
507 .unwrap();
508 }
509}