1#[cfg(windows)]
6pub mod hyperv;
7pub mod openvmm;
9pub mod vtl2_settings;
10
11use crate::PetriLogSource;
12use crate::PetriTestParams;
13use crate::ShutdownKind;
14use crate::disk_image::AgentImage;
15use crate::disk_image::SECTOR_SIZE;
16use crate::openhcl_diag::OpenHclDiagHandler;
17use crate::test::PetriPostTestHook;
18use crate::vtl2_settings::ControllerType;
19use crate::vtl2_settings::Vtl2LunBuilder;
20use crate::vtl2_settings::Vtl2StorageBackingDeviceBuilder;
21use crate::vtl2_settings::Vtl2StorageControllerBuilder;
22use async_trait::async_trait;
23use get_resources::ged::FirmwareEvent;
24use guid::Guid;
25use mesh::CancelContext;
26use openvmm_defs::config::Vtl2BaseAddressType;
27use pal_async::DefaultDriver;
28use pal_async::task::Spawn;
29use pal_async::task::Task;
30use pal_async::timer::PolledTimer;
31use petri_artifacts_common::tags::GuestQuirks;
32use petri_artifacts_common::tags::GuestQuirksInner;
33use petri_artifacts_common::tags::InitialRebootCondition;
34use petri_artifacts_common::tags::IsOpenhclIgvm;
35use petri_artifacts_common::tags::IsTestVmgs;
36use petri_artifacts_common::tags::MachineArch;
37use petri_artifacts_common::tags::OsFlavor;
38use petri_artifacts_core::ArtifactResolver;
39use petri_artifacts_core::ResolvedArtifact;
40use petri_artifacts_core::ResolvedOptionalArtifact;
41use pipette_client::PipetteClient;
42use std::collections::BTreeMap;
43use std::collections::HashMap;
44use std::collections::hash_map::DefaultHasher;
45use std::fmt::Debug;
46use std::hash::Hash;
47use std::hash::Hasher;
48use std::path::Path;
49use std::path::PathBuf;
50use std::sync::Arc;
51use std::time::Duration;
52use tempfile::TempPath;
53use vmgs_resources::GuestStateEncryptionPolicy;
54use vtl2_settings_proto::StorageController;
55use vtl2_settings_proto::Vtl2Settings;
56
57pub struct PetriVmArtifacts<T: PetriVmmBackend> {
60 pub backend: T,
62 pub firmware: Firmware,
64 pub arch: MachineArch,
66 pub agent_image: Option<AgentImage>,
68 pub openhcl_agent_image: Option<AgentImage>,
70}
71
72impl<T: PetriVmmBackend> PetriVmArtifacts<T> {
73 pub fn new(
77 resolver: &ArtifactResolver<'_>,
78 firmware: Firmware,
79 arch: MachineArch,
80 with_vtl0_pipette: bool,
81 ) -> Option<Self> {
82 if !T::check_compat(&firmware, arch) {
83 return None;
84 }
85
86 Some(Self {
87 backend: T::new(resolver),
88 arch,
89 agent_image: Some(if with_vtl0_pipette {
90 AgentImage::new(firmware.os_flavor()).with_pipette(resolver, arch)
91 } else {
92 AgentImage::new(firmware.os_flavor())
93 }),
94 openhcl_agent_image: if firmware.is_openhcl() {
95 Some(AgentImage::new(OsFlavor::Linux).with_pipette(resolver, arch))
96 } else {
97 None
98 },
99 firmware,
100 })
101 }
102}
103
104pub struct PetriVmBuilder<T: PetriVmmBackend> {
106 backend: T,
108 config: PetriVmConfig,
110 modify_vmm_config: Option<ModifyFn<T::VmmConfig>>,
112 resources: PetriVmResources,
114
115 guest_quirks: GuestQuirksInner,
117 vmm_quirks: VmmQuirks,
118
119 expected_boot_event: Option<FirmwareEvent>,
122 override_expect_reset: bool,
123
124 agent_image: Option<AgentImage>,
128 openhcl_agent_image: Option<AgentImage>,
130 boot_device_type: BootDeviceType,
132}
133
134impl<T: PetriVmmBackend> Debug for PetriVmBuilder<T> {
135 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136 f.debug_struct("PetriVmBuilder")
137 .field("backend", &self.backend)
138 .field("config", &self.config)
139 .field("modify_vmm_config", &self.modify_vmm_config.is_some())
140 .field("resources", &self.resources)
141 .field("guest_quirks", &self.guest_quirks)
142 .field("vmm_quirks", &self.vmm_quirks)
143 .field("expected_boot_event", &self.expected_boot_event)
144 .field("override_expect_reset", &self.override_expect_reset)
145 .field("agent_image", &self.agent_image)
146 .field("openhcl_agent_image", &self.openhcl_agent_image)
147 .field("boot_device_type", &self.boot_device_type)
148 .finish()
149 }
150}
151
152#[derive(Debug)]
154pub struct PetriVmConfig {
155 pub name: String,
157 pub arch: MachineArch,
159 pub host_log_levels: Option<OpenvmmLogConfig>,
161 pub firmware: Firmware,
163 pub memory: MemoryConfig,
165 pub proc_topology: ProcessorTopology,
167 pub vmgs: PetriVmgsResource,
169 pub tpm: Option<TpmConfig>,
171 pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
173}
174
175pub struct PetriVmProperties {
178 pub is_openhcl: bool,
180 pub is_isolated: bool,
182 pub is_pcat: bool,
184 pub is_linux_direct: bool,
186 pub using_vtl0_pipette: bool,
188 pub using_vpci: bool,
190 pub os_flavor: OsFlavor,
192}
193
194pub struct PetriVmRuntimeConfig {
196 pub vtl2_settings: Option<Vtl2Settings>,
198 pub ide_controllers: Option<[[Option<Drive>; 2]; 2]>,
200 pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
202}
203
204#[derive(Debug)]
206pub struct PetriVmResources {
207 driver: DefaultDriver,
208 log_source: PetriLogSource,
209}
210
211#[async_trait]
213pub trait PetriVmmBackend: Debug {
214 type VmmConfig;
216
217 type VmRuntime: PetriVmRuntime;
219
220 fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool;
223
224 fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks);
226
227 fn default_servicing_flags() -> OpenHclServicingFlags;
229
230 fn create_guest_dump_disk() -> anyhow::Result<
233 Option<(
234 Arc<TempPath>,
235 Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
236 )>,
237 >;
238
239 fn new(resolver: &ArtifactResolver<'_>) -> Self;
241
242 async fn run(
244 self,
245 config: PetriVmConfig,
246 modify_vmm_config: Option<ModifyFn<Self::VmmConfig>>,
247 resources: &PetriVmResources,
248 properties: PetriVmProperties,
249 ) -> anyhow::Result<(Self::VmRuntime, PetriVmRuntimeConfig)>;
250}
251
252pub(crate) const PETRI_IDE_BOOT_CONTROLLER_NUMBER: u32 = 0;
254pub(crate) const PETRI_IDE_BOOT_LUN: u8 = 0;
255pub(crate) const PETRI_IDE_BOOT_CONTROLLER: Guid =
256 guid::guid!("ca56751f-e643-4bef-bf54-f73678e8b7b5");
257
258pub(crate) const PETRI_SCSI_BOOT_LUN: u32 = 0;
260pub(crate) const PETRI_SCSI_PIPETTE_LUN: u32 = 1;
261pub(crate) const PETRI_SCSI_CRASH_LUN: u32 = 2;
262pub(crate) const PETRI_SCSI_VTL0_CONTROLLER: Guid =
264 guid::guid!("27b553e8-8b39-411b-a55f-839971a7884f");
265pub(crate) const PETRI_SCSI_VTL2_CONTROLLER: Guid =
267 guid::guid!("766e96f8-2ceb-437e-afe3-a93169e48a7c");
268pub(crate) const PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER: Guid =
270 guid::guid!("6c474f47-ed39-49e6-bbb9-142177a1da6e");
271
272pub(crate) const PETRI_NVME_BOOT_NSID: u32 = 37;
274pub(crate) const PETRI_NVME_BOOT_VTL0_CONTROLLER: Guid =
276 guid::guid!("e23a04e2-90f5-4852-bc9d-e7ac691b756c");
277pub(crate) const PETRI_NVME_BOOT_VTL2_CONTROLLER: Guid =
279 guid::guid!("92bc8346-718b-449a-8751-edbf3dcd27e4");
280
281pub struct PetriVm<T: PetriVmmBackend> {
283 resources: PetriVmResources,
284 runtime: T::VmRuntime,
285 watchdog_tasks: Vec<Task<()>>,
286 openhcl_diag_handler: Option<OpenHclDiagHandler>,
287
288 arch: MachineArch,
289 guest_quirks: GuestQuirksInner,
290 vmm_quirks: VmmQuirks,
291 expected_boot_event: Option<FirmwareEvent>,
292
293 config: PetriVmRuntimeConfig,
294}
295
296impl<T: PetriVmmBackend> PetriVmBuilder<T> {
297 pub fn new(
299 params: PetriTestParams<'_>,
300 artifacts: PetriVmArtifacts<T>,
301 driver: &DefaultDriver,
302 ) -> anyhow::Result<Self> {
303 let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
304 let expected_boot_event = artifacts.firmware.expected_boot_event();
305 let boot_device_type = match artifacts.firmware {
306 Firmware::LinuxDirect { .. } => BootDeviceType::None,
307 Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
308 Firmware::Pcat { .. } => BootDeviceType::Ide,
309 Firmware::OpenhclPcat { .. } => BootDeviceType::IdeViaScsi,
310 Firmware::Uefi {
311 guest: UefiGuest::None,
312 ..
313 }
314 | Firmware::OpenhclUefi {
315 guest: UefiGuest::None,
316 ..
317 } => BootDeviceType::None,
318 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
319 };
320
321 Ok(Self {
322 backend: artifacts.backend,
323 config: PetriVmConfig {
324 name: make_vm_safe_name(params.test_name),
325 arch: artifacts.arch,
326 host_log_levels: None,
327 firmware: artifacts.firmware,
328 memory: Default::default(),
329 proc_topology: Default::default(),
330
331 vmgs: PetriVmgsResource::Ephemeral,
332 tpm: None,
333 vmbus_storage_controllers: HashMap::new(),
334 },
335 modify_vmm_config: None,
336 resources: PetriVmResources {
337 driver: driver.clone(),
338 log_source: params.logger.clone(),
339 },
340
341 guest_quirks,
342 vmm_quirks,
343 expected_boot_event,
344 override_expect_reset: false,
345
346 agent_image: artifacts.agent_image,
347 openhcl_agent_image: artifacts.openhcl_agent_image,
348 boot_device_type,
349 }
350 .add_petri_scsi_controllers()
351 .add_guest_crash_disk(params.post_test_hooks))
352 }
353
354 fn add_petri_scsi_controllers(self) -> Self {
355 let builder = self.add_vmbus_storage_controller(
356 &PETRI_SCSI_VTL0_CONTROLLER,
357 Vtl::Vtl0,
358 VmbusStorageType::Scsi,
359 );
360
361 if builder.is_openhcl() {
362 builder.add_vmbus_storage_controller(
363 &PETRI_SCSI_VTL2_CONTROLLER,
364 Vtl::Vtl2,
365 VmbusStorageType::Scsi,
366 )
367 } else {
368 builder
369 }
370 }
371
372 fn add_guest_crash_disk(self, post_test_hooks: &mut Vec<PetriPostTestHook>) -> Self {
373 let logger = self.resources.log_source.clone();
374 let (disk, disk_hook) = matches!(
375 self.config.firmware.os_flavor(),
376 OsFlavor::Windows | OsFlavor::Linux
377 )
378 .then(|| T::create_guest_dump_disk().expect("failed to create guest dump disk"))
379 .flatten()
380 .unzip();
381
382 if let Some(disk_hook) = disk_hook {
383 post_test_hooks.push(PetriPostTestHook::new(
384 "extract guest crash dumps".into(),
385 move |test_passed| {
386 if test_passed {
387 return Ok(());
388 }
389 let mut disk = disk_hook()?;
390 let gpt = gptman::GPT::read_from(&mut disk, SECTOR_SIZE)?;
391 let partition = fscommon::StreamSlice::new(
392 &mut disk,
393 gpt[1].starting_lba * SECTOR_SIZE,
394 gpt[1].ending_lba * SECTOR_SIZE,
395 )?;
396 let fs = fatfs::FileSystem::new(partition, fatfs::FsOptions::new())?;
397 for entry in fs.root_dir().iter() {
398 let Ok(entry) = entry else {
399 tracing::warn!(?entry, "failed to read entry in guest crash dump disk");
400 continue;
401 };
402 if !entry.is_file() {
403 tracing::warn!(
404 ?entry,
405 "skipping non-file entry in guest crash dump disk"
406 );
407 continue;
408 }
409 logger.write_attachment(&entry.file_name(), entry.to_file())?;
410 }
411 Ok(())
412 },
413 ));
414 }
415
416 if let Some(disk) = disk {
417 self.add_vmbus_drive(
418 Drive::new(Some(Disk::Temporary(disk)), false),
419 &PETRI_SCSI_VTL0_CONTROLLER,
420 Some(PETRI_SCSI_CRASH_LUN),
421 )
422 } else {
423 self
424 }
425 }
426
427 fn add_agent_disks(self) -> Self {
428 self.add_agent_disk_inner(Vtl::Vtl0)
429 .add_agent_disk_inner(Vtl::Vtl2)
430 }
431
432 fn add_agent_disk_inner(self, target_vtl: Vtl) -> Self {
433 let (agent_image, controller_id) = match target_vtl {
434 Vtl::Vtl0 => (self.agent_image.as_ref(), PETRI_SCSI_VTL0_CONTROLLER),
435 Vtl::Vtl1 => panic!("no VTL1 agent disk"),
436 Vtl::Vtl2 => (
437 self.openhcl_agent_image.as_ref(),
438 PETRI_SCSI_VTL2_CONTROLLER,
439 ),
440 };
441
442 if let Some(agent_disk) = agent_image.and_then(|i| {
443 i.build(crate::disk_image::ImageType::Vhd)
444 .expect("failed to build agent image")
445 }) {
446 self.add_vmbus_drive(
447 Drive::new(
448 Some(Disk::Temporary(Arc::new(agent_disk.into_temp_path()))),
449 false,
450 ),
451 &controller_id,
452 Some(PETRI_SCSI_PIPETTE_LUN),
453 )
454 } else {
455 self
456 }
457 }
458
459 fn add_boot_disk(mut self) -> Self {
460 if self.boot_device_type.requires_vtl2() && !self.is_openhcl() {
461 panic!("boot device type {:?} requires vtl2", self.boot_device_type);
462 }
463
464 if self.boot_device_type.requires_vpci_boot() {
465 self.config
466 .firmware
467 .uefi_config_mut()
468 .expect("vpci boot requires uefi")
469 .enable_vpci_boot = true;
470 }
471
472 if let Some(boot_drive) = self.config.firmware.boot_drive() {
473 match self.boot_device_type {
474 BootDeviceType::None => unreachable!(),
475 BootDeviceType::Ide => self.add_ide_drive(
476 boot_drive,
477 PETRI_IDE_BOOT_CONTROLLER_NUMBER,
478 PETRI_IDE_BOOT_LUN,
479 ),
480 BootDeviceType::IdeViaScsi => self
481 .add_vmbus_drive(
482 boot_drive,
483 &PETRI_SCSI_VTL2_CONTROLLER,
484 Some(PETRI_SCSI_BOOT_LUN),
485 )
486 .add_vtl2_storage_controller(
487 Vtl2StorageControllerBuilder::new(ControllerType::Ide)
488 .with_instance_id(PETRI_IDE_BOOT_CONTROLLER)
489 .add_lun(
490 Vtl2LunBuilder::disk()
491 .with_channel(PETRI_IDE_BOOT_CONTROLLER_NUMBER)
492 .with_location(PETRI_IDE_BOOT_LUN as u32)
493 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
494 ControllerType::Scsi,
495 PETRI_SCSI_VTL2_CONTROLLER,
496 PETRI_SCSI_BOOT_LUN,
497 )),
498 )
499 .build(),
500 ),
501 BootDeviceType::IdeViaNvme => todo!(),
502 BootDeviceType::Scsi => self.add_vmbus_drive(
503 boot_drive,
504 &PETRI_SCSI_VTL0_CONTROLLER,
505 Some(PETRI_SCSI_BOOT_LUN),
506 ),
507 BootDeviceType::ScsiViaScsi => self
508 .add_vmbus_drive(
509 boot_drive,
510 &PETRI_SCSI_VTL2_CONTROLLER,
511 Some(PETRI_SCSI_BOOT_LUN),
512 )
513 .add_vtl2_storage_controller(
514 Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
515 .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
516 .add_lun(
517 Vtl2LunBuilder::disk()
518 .with_location(PETRI_SCSI_BOOT_LUN)
519 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
520 ControllerType::Scsi,
521 PETRI_SCSI_VTL2_CONTROLLER,
522 PETRI_SCSI_BOOT_LUN,
523 )),
524 )
525 .build(),
526 ),
527 BootDeviceType::ScsiViaNvme => self
528 .add_vmbus_storage_controller(
529 &PETRI_NVME_BOOT_VTL2_CONTROLLER,
530 Vtl::Vtl2,
531 VmbusStorageType::Nvme,
532 )
533 .add_vmbus_drive(
534 boot_drive,
535 &PETRI_NVME_BOOT_VTL2_CONTROLLER,
536 Some(PETRI_NVME_BOOT_NSID),
537 )
538 .add_vtl2_storage_controller(
539 Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
540 .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
541 .add_lun(
542 Vtl2LunBuilder::disk()
543 .with_location(PETRI_SCSI_BOOT_LUN)
544 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
545 ControllerType::Nvme,
546 PETRI_NVME_BOOT_VTL2_CONTROLLER,
547 PETRI_NVME_BOOT_NSID,
548 )),
549 )
550 .build(),
551 ),
552 BootDeviceType::Nvme => self
553 .add_vmbus_storage_controller(
554 &PETRI_NVME_BOOT_VTL0_CONTROLLER,
555 Vtl::Vtl0,
556 VmbusStorageType::Nvme,
557 )
558 .add_vmbus_drive(
559 boot_drive,
560 &PETRI_NVME_BOOT_VTL0_CONTROLLER,
561 Some(PETRI_NVME_BOOT_NSID),
562 ),
563 BootDeviceType::NvmeViaScsi => todo!(),
564 BootDeviceType::NvmeViaNvme => todo!(),
565 }
566 } else {
567 self
568 }
569 }
570
571 pub fn properties(&self) -> PetriVmProperties {
573 PetriVmProperties {
574 is_openhcl: self.config.firmware.is_openhcl(),
575 is_isolated: self.config.firmware.isolation().is_some(),
576 is_pcat: self.config.firmware.is_pcat(),
577 is_linux_direct: self.config.firmware.is_linux_direct(),
578 using_vtl0_pipette: self.using_vtl0_pipette(),
579 using_vpci: self.boot_device_type.requires_vpci_boot(),
580 os_flavor: self.config.firmware.os_flavor(),
581 }
582 }
583
584 pub fn using_vtl0_pipette(&self) -> bool {
586 self.agent_image
587 .as_ref()
588 .is_some_and(|x| x.contains_pipette())
589 }
590
591 pub async fn run_without_agent(self) -> anyhow::Result<PetriVm<T>> {
595 self.run_core().await
596 }
597
598 pub async fn run(self) -> anyhow::Result<(PetriVm<T>, PipetteClient)> {
601 assert!(self.using_vtl0_pipette());
602
603 let mut vm = self.run_core().await?;
604 let client = vm.wait_for_agent().await?;
605 Ok((vm, client))
606 }
607
608 async fn run_core(mut self) -> anyhow::Result<PetriVm<T>> {
609 self = self.add_boot_disk().add_agent_disks();
612 tracing::debug!(builder = ?self);
613
614 let arch = self.config.arch;
615 let expect_reset = self.expect_reset();
616 let properties = self.properties();
617
618 let (mut runtime, config) = self
619 .backend
620 .run(
621 self.config,
622 self.modify_vmm_config,
623 &self.resources,
624 properties,
625 )
626 .await?;
627 let openhcl_diag_handler = runtime.openhcl_diag();
628 let watchdog_tasks = Self::start_watchdog_tasks(&self.resources, &mut runtime)?;
629
630 let mut vm = PetriVm {
631 resources: self.resources,
632 runtime,
633 watchdog_tasks,
634 openhcl_diag_handler,
635
636 arch,
637 guest_quirks: self.guest_quirks,
638 vmm_quirks: self.vmm_quirks,
639 expected_boot_event: self.expected_boot_event,
640
641 config,
642 };
643
644 if expect_reset {
645 vm.wait_for_reset_core().await?;
646 }
647
648 vm.wait_for_expected_boot_event().await?;
649
650 Ok(vm)
651 }
652
653 fn expect_reset(&self) -> bool {
654 self.override_expect_reset
655 || matches!(
656 (
657 self.guest_quirks.initial_reboot,
658 self.expected_boot_event,
659 &self.config.firmware,
660 &self.config.tpm,
661 ),
662 (
663 Some(InitialRebootCondition::Always),
664 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
665 _,
666 _,
667 ) | (
668 Some(InitialRebootCondition::WithTpm),
669 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
670 _,
671 Some(_),
672 )
673 )
674 }
675
676 fn start_watchdog_tasks(
677 resources: &PetriVmResources,
678 runtime: &mut T::VmRuntime,
679 ) -> anyhow::Result<Vec<Task<()>>> {
680 let mut tasks = Vec::new();
681
682 {
683 const TIMEOUT_DURATION_MINUTES: u64 = 10;
684 const TIMER_DURATION: Duration = Duration::from_secs(TIMEOUT_DURATION_MINUTES * 60);
685 let log_source = resources.log_source.clone();
686 let inspect_task =
687 |name,
688 driver: &DefaultDriver,
689 inspect: std::pin::Pin<Box<dyn Future<Output = _> + Send>>| {
690 driver.spawn(format!("petri-watchdog-inspect-{name}"), async move {
691 if CancelContext::new()
692 .with_timeout(Duration::from_secs(10))
693 .until_cancelled(save_inspect(name, inspect, &log_source))
694 .await
695 .is_err()
696 {
697 tracing::warn!(name, "Failed to collect inspect data within timeout");
698 }
699 })
700 };
701
702 let driver = resources.driver.clone();
703 let vmm_inspector = runtime.inspector();
704 let openhcl_diag_handler = runtime.openhcl_diag();
705 tasks.push(resources.driver.spawn("timer-watchdog", async move {
706 PolledTimer::new(&driver).sleep(TIMER_DURATION).await;
707 tracing::warn!("Test timeout reached after {TIMEOUT_DURATION_MINUTES} minutes, collecting diagnostics.");
708 let mut timeout_tasks = Vec::new();
709 if let Some(inspector) = vmm_inspector {
710 timeout_tasks.push(inspect_task.clone()("vmm", &driver, Box::pin(async move { inspector.inspect_all().await })) );
711 }
712 if let Some(openhcl_diag_handler) = openhcl_diag_handler {
713 timeout_tasks.push(inspect_task("openhcl", &driver, Box::pin(async move { openhcl_diag_handler.inspect("", None, None).await })));
714 }
715 futures::future::join_all(timeout_tasks).await;
716 tracing::error!("Test time out diagnostics collection complete, aborting.");
717 panic!("Test timed out");
718 }));
719 }
720
721 if let Some(mut framebuffer_access) = runtime.take_framebuffer_access() {
722 let mut timer = PolledTimer::new(&resources.driver);
723 let log_source = resources.log_source.clone();
724
725 tasks.push(
726 resources
727 .driver
728 .spawn("petri-watchdog-screenshot", async move {
729 let mut image = Vec::new();
730 let mut last_image = Vec::new();
731 loop {
732 timer.sleep(Duration::from_secs(2)).await;
733 tracing::trace!("Taking screenshot.");
734
735 let VmScreenshotMeta {
736 color,
737 width,
738 height,
739 } = match framebuffer_access.screenshot(&mut image).await {
740 Ok(Some(meta)) => meta,
741 Ok(None) => {
742 tracing::debug!("VM off, skipping screenshot.");
743 continue;
744 }
745 Err(e) => {
746 tracing::error!(?e, "Failed to take screenshot");
747 continue;
748 }
749 };
750
751 if image == last_image {
752 tracing::debug!("No change in framebuffer, skipping screenshot.");
753 continue;
754 }
755
756 let r =
757 log_source
758 .create_attachment("screenshot.png")
759 .and_then(|mut f| {
760 image::write_buffer_with_format(
761 &mut f,
762 &image,
763 width.into(),
764 height.into(),
765 color,
766 image::ImageFormat::Png,
767 )
768 .map_err(Into::into)
769 });
770
771 if let Err(e) = r {
772 tracing::error!(?e, "Failed to save screenshot");
773 } else {
774 tracing::info!("Screenshot saved.");
775 }
776
777 std::mem::swap(&mut image, &mut last_image);
778 }
779 }),
780 );
781 }
782
783 Ok(tasks)
784 }
785
786 pub fn with_expect_boot_failure(mut self) -> Self {
789 self.expected_boot_event = Some(FirmwareEvent::BootFailed);
790 self
791 }
792
793 pub fn with_expect_no_boot_event(mut self) -> Self {
796 self.expected_boot_event = None;
797 self
798 }
799
800 pub fn with_expect_reset(mut self) -> Self {
804 self.override_expect_reset = true;
805 self
806 }
807
808 pub fn with_secure_boot(mut self) -> Self {
810 self.config
811 .firmware
812 .uefi_config_mut()
813 .expect("Secure boot is only supported for UEFI firmware.")
814 .secure_boot_enabled = true;
815
816 match self.os_flavor() {
817 OsFlavor::Windows => self.with_windows_secure_boot_template(),
818 OsFlavor::Linux => self.with_uefi_ca_secure_boot_template(),
819 _ => panic!(
820 "Secure boot unsupported for OS flavor {:?}",
821 self.os_flavor()
822 ),
823 }
824 }
825
826 pub fn with_windows_secure_boot_template(mut self) -> Self {
828 self.config
829 .firmware
830 .uefi_config_mut()
831 .expect("Secure boot is only supported for UEFI firmware.")
832 .secure_boot_template = Some(SecureBootTemplate::MicrosoftWindows);
833 self
834 }
835
836 pub fn with_uefi_ca_secure_boot_template(mut self) -> Self {
838 self.config
839 .firmware
840 .uefi_config_mut()
841 .expect("Secure boot is only supported for UEFI firmware.")
842 .secure_boot_template = Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority);
843 self
844 }
845
846 pub fn with_processor_topology(mut self, topology: ProcessorTopology) -> Self {
848 self.config.proc_topology = topology;
849 self
850 }
851
852 pub fn with_memory(mut self, memory: MemoryConfig) -> Self {
854 self.config.memory = memory;
855 self
856 }
857
858 pub fn with_vtl2_base_address_type(mut self, address_type: Vtl2BaseAddressType) -> Self {
863 self.config
864 .firmware
865 .openhcl_config_mut()
866 .expect("OpenHCL firmware is required to set custom VTL2 address type.")
867 .vtl2_base_address_type = Some(address_type);
868 self
869 }
870
871 pub fn with_custom_openhcl(mut self, artifact: ResolvedArtifact<impl IsOpenhclIgvm>) -> Self {
873 match &mut self.config.firmware {
874 Firmware::OpenhclLinuxDirect { igvm_path, .. }
875 | Firmware::OpenhclPcat { igvm_path, .. }
876 | Firmware::OpenhclUefi { igvm_path, .. } => {
877 *igvm_path = artifact.erase();
878 }
879 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } | Firmware::Pcat { .. } => {
880 panic!("Custom OpenHCL is only supported for OpenHCL firmware.")
881 }
882 }
883 self
884 }
885
886 pub fn with_openhcl_command_line(mut self, additional_command_line: &str) -> Self {
888 append_cmdline(
889 &mut self
890 .config
891 .firmware
892 .openhcl_config_mut()
893 .expect("OpenHCL command line is only supported for OpenHCL firmware.")
894 .custom_command_line,
895 additional_command_line,
896 );
897 self
898 }
899
900 pub fn with_confidential_filtering(self) -> Self {
902 if !self.config.firmware.is_openhcl() {
903 panic!("Confidential filtering is only supported for OpenHCL");
904 }
905 self.with_openhcl_command_line(&format!(
906 "{}=1 {}=0",
907 underhill_confidentiality::OPENHCL_CONFIDENTIAL_ENV_VAR_NAME,
908 underhill_confidentiality::OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME
909 ))
910 }
911
912 pub fn with_openhcl_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
914 self.config
915 .firmware
916 .openhcl_config_mut()
917 .expect("OpenHCL firmware is required to set custom OpenHCL log levels.")
918 .log_levels = levels;
919 self
920 }
921
922 pub fn with_host_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
926 if let OpenvmmLogConfig::Custom(ref custom_levels) = levels {
927 for key in custom_levels.keys() {
928 if !["OPENVMM_LOG", "OPENVMM_SHOW_SPANS"].contains(&key.as_str()) {
929 panic!("Unsupported OpenVMM log level key: {}", key);
930 }
931 }
932 }
933
934 self.config.host_log_levels = Some(levels.clone());
935 self
936 }
937
938 pub fn with_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
940 self.agent_image
941 .as_mut()
942 .expect("no guest pipette")
943 .add_file(name, artifact);
944 self
945 }
946
947 pub fn with_openhcl_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
949 self.openhcl_agent_image
950 .as_mut()
951 .expect("no openhcl pipette")
952 .add_file(name, artifact);
953 self
954 }
955
956 pub fn with_uefi_frontpage(mut self, enable: bool) -> Self {
958 self.config
959 .firmware
960 .uefi_config_mut()
961 .expect("UEFI frontpage is only supported for UEFI firmware.")
962 .disable_frontpage = !enable;
963 self
964 }
965
966 pub fn with_default_boot_always_attempt(mut self, enable: bool) -> Self {
968 self.config
969 .firmware
970 .uefi_config_mut()
971 .expect("Default boot always attempt is only supported for UEFI firmware.")
972 .default_boot_always_attempt = enable;
973 self
974 }
975
976 pub fn with_vmbus_redirect(mut self, enable: bool) -> Self {
978 self.config
979 .firmware
980 .openhcl_config_mut()
981 .expect("VMBus redirection is only supported for OpenHCL firmware.")
982 .vmbus_redirect = enable;
983 self
984 }
985
986 pub fn with_guest_state_lifetime(
988 mut self,
989 guest_state_lifetime: PetriGuestStateLifetime,
990 ) -> Self {
991 let disk = match self.config.vmgs {
992 PetriVmgsResource::Disk(disk)
993 | PetriVmgsResource::ReprovisionOnFailure(disk)
994 | PetriVmgsResource::Reprovision(disk) => disk,
995 PetriVmgsResource::Ephemeral => PetriVmgsDisk::default(),
996 };
997 self.config.vmgs = match guest_state_lifetime {
998 PetriGuestStateLifetime::Disk => PetriVmgsResource::Disk(disk),
999 PetriGuestStateLifetime::ReprovisionOnFailure => {
1000 PetriVmgsResource::ReprovisionOnFailure(disk)
1001 }
1002 PetriGuestStateLifetime::Reprovision => PetriVmgsResource::Reprovision(disk),
1003 PetriGuestStateLifetime::Ephemeral => {
1004 if !matches!(disk.disk, Disk::Memory(_)) {
1005 panic!("attempted to use ephemeral guest state after specifying backing vmgs")
1006 }
1007 PetriVmgsResource::Ephemeral
1008 }
1009 };
1010 self
1011 }
1012
1013 pub fn with_guest_state_encryption(mut self, policy: GuestStateEncryptionPolicy) -> Self {
1015 match &mut self.config.vmgs {
1016 PetriVmgsResource::Disk(vmgs)
1017 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1018 | PetriVmgsResource::Reprovision(vmgs) => {
1019 vmgs.encryption_policy = policy;
1020 }
1021 PetriVmgsResource::Ephemeral => {
1022 panic!("attempted to encrypt ephemeral guest state")
1023 }
1024 }
1025 self
1026 }
1027
1028 pub fn with_initial_vmgs(self, disk: ResolvedArtifact<impl IsTestVmgs>) -> Self {
1030 self.with_backing_vmgs(Disk::Differencing(disk.into()))
1031 }
1032
1033 pub fn with_persistent_vmgs(self, disk: impl AsRef<Path>) -> Self {
1035 self.with_backing_vmgs(Disk::Persistent(disk.as_ref().to_path_buf()))
1036 }
1037
1038 fn with_backing_vmgs(mut self, disk: Disk) -> Self {
1039 match &mut self.config.vmgs {
1040 PetriVmgsResource::Disk(vmgs)
1041 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1042 | PetriVmgsResource::Reprovision(vmgs) => {
1043 if !matches!(vmgs.disk, Disk::Memory(_)) {
1044 panic!("already specified a backing vmgs file");
1045 }
1046 vmgs.disk = disk;
1047 }
1048 PetriVmgsResource::Ephemeral => {
1049 panic!("attempted to specify a backing vmgs with ephemeral guest state")
1050 }
1051 }
1052 self
1053 }
1054
1055 pub fn with_boot_device_type(mut self, boot: BootDeviceType) -> Self {
1059 self.boot_device_type = boot;
1060 self
1061 }
1062
1063 pub fn with_tpm(mut self, enable: bool) -> Self {
1065 if enable {
1066 self.config.tpm.get_or_insert_default();
1067 } else {
1068 self.config.tpm = None;
1069 }
1070 self
1071 }
1072
1073 pub fn with_tpm_state_persistence(mut self, tpm_state_persistence: bool) -> Self {
1075 self.config
1076 .tpm
1077 .as_mut()
1078 .expect("TPM persistence requires a TPM")
1079 .no_persistent_secrets = !tpm_state_persistence;
1080 self
1081 }
1082
1083 pub fn with_custom_vtl2_settings(
1087 mut self,
1088 f: impl FnOnce(&mut Vtl2Settings) + 'static + Send + Sync,
1089 ) -> Self {
1090 f(self
1091 .config
1092 .firmware
1093 .vtl2_settings()
1094 .expect("Custom VTL 2 settings are only supported with OpenHCL"));
1095 self
1096 }
1097
1098 pub fn add_vtl2_storage_controller(self, controller: StorageController) -> Self {
1100 self.with_custom_vtl2_settings(move |v| {
1101 v.dynamic
1102 .as_mut()
1103 .unwrap()
1104 .storage_controllers
1105 .push(controller)
1106 })
1107 }
1108
1109 pub fn add_vmbus_storage_controller(
1111 mut self,
1112 id: &Guid,
1113 target_vtl: Vtl,
1114 controller_type: VmbusStorageType,
1115 ) -> Self {
1116 if self
1117 .config
1118 .vmbus_storage_controllers
1119 .insert(
1120 *id,
1121 VmbusStorageController::new(target_vtl, controller_type),
1122 )
1123 .is_some()
1124 {
1125 panic!("storage controller {id} already existed");
1126 }
1127 self
1128 }
1129
1130 pub fn add_vmbus_drive(
1132 mut self,
1133 drive: Drive,
1134 controller_id: &Guid,
1135 controller_location: Option<u32>,
1136 ) -> Self {
1137 let controller = self
1138 .config
1139 .vmbus_storage_controllers
1140 .get_mut(controller_id)
1141 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
1142
1143 _ = controller.set_drive(controller_location, drive, false);
1144
1145 self
1146 }
1147
1148 pub fn add_ide_drive(
1150 mut self,
1151 drive: Drive,
1152 controller_number: u32,
1153 controller_location: u8,
1154 ) -> Self {
1155 self.config
1156 .firmware
1157 .ide_controllers_mut()
1158 .expect("Host IDE requires PCAT with no HCL")[controller_number as usize]
1159 [controller_location as usize] = Some(drive);
1160
1161 self
1162 }
1163
1164 pub fn os_flavor(&self) -> OsFlavor {
1166 self.config.firmware.os_flavor()
1167 }
1168
1169 pub fn is_openhcl(&self) -> bool {
1171 self.config.firmware.is_openhcl()
1172 }
1173
1174 pub fn isolation(&self) -> Option<IsolationType> {
1176 self.config.firmware.isolation()
1177 }
1178
1179 pub fn arch(&self) -> MachineArch {
1181 self.config.arch
1182 }
1183
1184 pub fn default_servicing_flags(&self) -> OpenHclServicingFlags {
1186 T::default_servicing_flags()
1187 }
1188
1189 pub fn modify_backend(
1191 mut self,
1192 f: impl FnOnce(T::VmmConfig) -> T::VmmConfig + 'static + Send,
1193 ) -> Self {
1194 if self.modify_vmm_config.is_some() {
1195 panic!("only one modify_backend allowed");
1196 }
1197 self.modify_vmm_config = Some(ModifyFn(Box::new(f)));
1198 self
1199 }
1200}
1201
1202impl<T: PetriVmmBackend> PetriVm<T> {
1203 pub async fn teardown(self) -> anyhow::Result<()> {
1205 tracing::info!("Tearing down VM...");
1206 self.runtime.teardown().await
1207 }
1208
1209 pub async fn wait_for_halt(&mut self) -> anyhow::Result<PetriHaltReason> {
1211 tracing::info!("Waiting for VM to halt...");
1212 let halt_reason = self.runtime.wait_for_halt(false).await?;
1213 tracing::info!("VM halted: {halt_reason:?}. Cancelling watchdogs...");
1214 futures::future::join_all(self.watchdog_tasks.drain(..).map(|t| t.cancel())).await;
1215 Ok(halt_reason)
1216 }
1217
1218 pub async fn wait_for_clean_shutdown(&mut self) -> anyhow::Result<()> {
1220 let halt_reason = self.wait_for_halt().await?;
1221 if halt_reason != PetriHaltReason::PowerOff {
1222 anyhow::bail!("Expected PowerOff, got {halt_reason:?}");
1223 }
1224 tracing::info!("VM was cleanly powered off and torn down.");
1225 Ok(())
1226 }
1227
1228 pub async fn wait_for_teardown(mut self) -> anyhow::Result<PetriHaltReason> {
1231 let halt_reason = self.wait_for_halt().await?;
1232 self.teardown().await?;
1233 Ok(halt_reason)
1234 }
1235
1236 pub async fn wait_for_clean_teardown(mut self) -> anyhow::Result<()> {
1238 self.wait_for_clean_shutdown().await?;
1239 self.teardown().await
1240 }
1241
1242 pub async fn wait_for_reset_no_agent(&mut self) -> anyhow::Result<()> {
1244 self.wait_for_reset_core().await?;
1245 self.wait_for_expected_boot_event().await?;
1246 Ok(())
1247 }
1248
1249 pub async fn wait_for_reset(&mut self) -> anyhow::Result<PipetteClient> {
1251 self.wait_for_reset_no_agent().await?;
1252 self.wait_for_agent().await
1253 }
1254
1255 async fn wait_for_reset_core(&mut self) -> anyhow::Result<()> {
1256 tracing::info!("Waiting for VM to reset...");
1257 let halt_reason = self.runtime.wait_for_halt(true).await?;
1258 if halt_reason != PetriHaltReason::Reset {
1259 anyhow::bail!("Expected reset, got {halt_reason:?}");
1260 }
1261 tracing::info!("VM reset.");
1262 Ok(())
1263 }
1264
1265 pub async fn inspect_openhcl(
1276 &self,
1277 path: impl Into<String>,
1278 depth: Option<usize>,
1279 timeout: Option<Duration>,
1280 ) -> anyhow::Result<inspect::Node> {
1281 self.openhcl_diag()?
1282 .inspect(path.into().as_str(), depth, timeout)
1283 .await
1284 }
1285
1286 pub async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()> {
1288 self.inspect_openhcl("", None, None).await.map(|_| ())
1289 }
1290
1291 pub async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()> {
1297 self.openhcl_diag()?.wait_for_vtl2().await
1298 }
1299
1300 pub async fn kmsg(&self) -> anyhow::Result<diag_client::kmsg_stream::KmsgStream> {
1302 self.openhcl_diag()?.kmsg().await
1303 }
1304
1305 pub async fn openhcl_core_dump(&self, name: &str, path: &Path) -> anyhow::Result<()> {
1308 self.openhcl_diag()?.core_dump(name, path).await
1309 }
1310
1311 pub async fn openhcl_crash(&self, name: &str) -> anyhow::Result<()> {
1313 self.openhcl_diag()?.crash(name).await
1314 }
1315
1316 async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient> {
1319 self.runtime.wait_for_enlightened_shutdown_ready().await?;
1326 self.runtime.wait_for_agent(false).await
1327 }
1328
1329 pub async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient> {
1333 self.launch_vtl2_pipette().await?;
1335 self.runtime.wait_for_agent(true).await
1336 }
1337
1338 async fn wait_for_expected_boot_event(&mut self) -> anyhow::Result<()> {
1345 if let Some(expected_event) = self.expected_boot_event {
1346 let event = self.wait_for_boot_event().await?;
1347
1348 anyhow::ensure!(
1349 event == expected_event,
1350 "Did not receive expected boot event"
1351 );
1352 } else {
1353 tracing::warn!("Boot event not emitted for configured firmware or manually ignored.");
1354 }
1355
1356 Ok(())
1357 }
1358
1359 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
1362 tracing::info!("Waiting for boot event...");
1363 let boot_event = loop {
1364 match CancelContext::new()
1365 .with_timeout(self.vmm_quirks.flaky_boot.unwrap_or(Duration::MAX))
1366 .until_cancelled(self.runtime.wait_for_boot_event())
1367 .await
1368 {
1369 Ok(res) => break res?,
1370 Err(_) => {
1371 tracing::error!("Did not get boot event in required time, resetting...");
1372 if let Some(inspector) = self.runtime.inspector() {
1373 save_inspect(
1374 "vmm",
1375 Box::pin(async move { inspector.inspect_all().await }),
1376 &self.resources.log_source,
1377 )
1378 .await;
1379 }
1380
1381 self.runtime.reset().await?;
1382 continue;
1383 }
1384 }
1385 };
1386 tracing::info!("Got boot event: {boot_event:?}");
1387 Ok(boot_event)
1388 }
1389
1390 pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
1393 tracing::info!("Waiting for enlightened shutdown to be ready");
1394 self.runtime.wait_for_enlightened_shutdown_ready().await?;
1395
1396 let mut wait_time = Duration::from_secs(10);
1402
1403 if let Some(duration) = self.guest_quirks.hyperv_shutdown_ic_sleep {
1405 wait_time += duration;
1406 }
1407
1408 tracing::info!(
1409 "Shutdown IC reported ready, waiting for an extra {}s",
1410 wait_time.as_secs()
1411 );
1412 PolledTimer::new(&self.resources.driver)
1413 .sleep(wait_time)
1414 .await;
1415
1416 tracing::info!("Sending enlightened shutdown command");
1417 self.runtime.send_enlightened_shutdown(kind).await
1418 }
1419
1420 pub async fn restart_openhcl(
1423 &mut self,
1424 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1425 flags: OpenHclServicingFlags,
1426 ) -> anyhow::Result<()> {
1427 self.runtime
1428 .restart_openhcl(&new_openhcl.erase(), flags)
1429 .await
1430 }
1431
1432 pub async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()> {
1435 self.runtime.update_command_line(command_line).await
1436 }
1437
1438 pub async fn save_openhcl(
1441 &mut self,
1442 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1443 flags: OpenHclServicingFlags,
1444 ) -> anyhow::Result<()> {
1445 self.runtime.save_openhcl(&new_openhcl.erase(), flags).await
1446 }
1447
1448 pub async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
1451 self.runtime.restore_openhcl().await
1452 }
1453
1454 pub fn arch(&self) -> MachineArch {
1456 self.arch
1457 }
1458
1459 pub fn backend(&mut self) -> &mut T::VmRuntime {
1461 &mut self.runtime
1462 }
1463
1464 async fn launch_vtl2_pipette(&self) -> anyhow::Result<()> {
1465 tracing::debug!("Launching VTL 2 pipette...");
1466
1467 let res = self
1469 .openhcl_diag()?
1470 .run_vtl2_command("sh", &["-c", "mkdir /cidata && mount LABEL=cidata /cidata"])
1471 .await?;
1472
1473 if !res.exit_status.success() {
1474 anyhow::bail!("Failed to mount VTL 2 pipette drive: {:?}", res);
1475 }
1476
1477 let res = self
1478 .openhcl_diag()?
1479 .run_detached_vtl2_command("sh", &["-c", "/cidata/pipette 2>&1 | logger &"])
1480 .await?;
1481
1482 if !res.success() {
1483 anyhow::bail!("Failed to spawn VTL 2 pipette: {:?}", res);
1484 }
1485
1486 Ok(())
1487 }
1488
1489 fn openhcl_diag(&self) -> anyhow::Result<&OpenHclDiagHandler> {
1490 if let Some(ohd) = self.openhcl_diag_handler.as_ref() {
1491 Ok(ohd)
1492 } else {
1493 anyhow::bail!("VM is not configured with OpenHCL")
1494 }
1495 }
1496
1497 pub async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
1499 self.runtime.get_guest_state_file().await
1500 }
1501
1502 pub async fn modify_vtl2_settings(
1504 &mut self,
1505 f: impl FnOnce(&mut Vtl2Settings),
1506 ) -> anyhow::Result<()> {
1507 if self.openhcl_diag_handler.is_none() {
1508 panic!("Custom VTL 2 settings are only supported with OpenHCL");
1509 }
1510 f(self
1511 .config
1512 .vtl2_settings
1513 .get_or_insert_with(default_vtl2_settings));
1514 self.runtime
1515 .set_vtl2_settings(self.config.vtl2_settings.as_ref().unwrap())
1516 .await
1517 }
1518
1519 pub fn get_vmbus_storage_controllers(&self) -> &HashMap<Guid, VmbusStorageController> {
1521 &self.config.vmbus_storage_controllers
1522 }
1523
1524 pub async fn set_vmbus_drive(
1526 &mut self,
1527 drive: Drive,
1528 controller_id: &Guid,
1529 controller_location: Option<u32>,
1530 ) -> anyhow::Result<()> {
1531 let controller = self
1532 .config
1533 .vmbus_storage_controllers
1534 .get_mut(controller_id)
1535 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
1536
1537 let controller_location = controller.set_drive(controller_location, drive, true);
1538 let disk = controller.drives.get(&controller_location).unwrap();
1539
1540 self.runtime
1541 .set_vmbus_drive(disk, controller_id, controller_location)
1542 .await?;
1543
1544 Ok(())
1545 }
1546}
1547
1548#[async_trait]
1550pub trait PetriVmRuntime: Send + Sync + 'static {
1551 type VmInspector: PetriVmInspector;
1553 type VmFramebufferAccess: PetriVmFramebufferAccess;
1555
1556 async fn teardown(self) -> anyhow::Result<()>;
1558 async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReason>;
1561 async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>;
1563 fn openhcl_diag(&self) -> Option<OpenHclDiagHandler>;
1565 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent>;
1568 async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()>;
1571 async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
1573 async fn restart_openhcl(
1576 &mut self,
1577 new_openhcl: &ResolvedArtifact,
1578 flags: OpenHclServicingFlags,
1579 ) -> anyhow::Result<()>;
1580 async fn save_openhcl(
1584 &mut self,
1585 new_openhcl: &ResolvedArtifact,
1586 flags: OpenHclServicingFlags,
1587 ) -> anyhow::Result<()>;
1588 async fn restore_openhcl(&mut self) -> anyhow::Result<()>;
1591 async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()>;
1594 fn inspector(&self) -> Option<Self::VmInspector> {
1596 None
1597 }
1598 fn take_framebuffer_access(&mut self) -> Option<Self::VmFramebufferAccess> {
1601 None
1602 }
1603 async fn reset(&mut self) -> anyhow::Result<()>;
1605 async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
1607 Ok(None)
1608 }
1609 async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()>;
1611 async fn set_vmbus_drive(
1613 &mut self,
1614 disk: &Drive,
1615 controller_id: &Guid,
1616 controller_location: u32,
1617 ) -> anyhow::Result<()>;
1618}
1619
1620#[async_trait]
1622pub trait PetriVmInspector: Send + Sync + 'static {
1623 async fn inspect_all(&self) -> anyhow::Result<inspect::Node>;
1625}
1626
1627pub struct NoPetriVmInspector;
1629#[async_trait]
1630impl PetriVmInspector for NoPetriVmInspector {
1631 async fn inspect_all(&self) -> anyhow::Result<inspect::Node> {
1632 unreachable!()
1633 }
1634}
1635
1636pub struct VmScreenshotMeta {
1638 pub color: image::ExtendedColorType,
1640 pub width: u16,
1642 pub height: u16,
1644}
1645
1646#[async_trait]
1648pub trait PetriVmFramebufferAccess: Send + 'static {
1649 async fn screenshot(&mut self, image: &mut Vec<u8>)
1652 -> anyhow::Result<Option<VmScreenshotMeta>>;
1653}
1654
1655pub struct NoPetriVmFramebufferAccess;
1657#[async_trait]
1658impl PetriVmFramebufferAccess for NoPetriVmFramebufferAccess {
1659 async fn screenshot(
1660 &mut self,
1661 _image: &mut Vec<u8>,
1662 ) -> anyhow::Result<Option<VmScreenshotMeta>> {
1663 unreachable!()
1664 }
1665}
1666
1667#[derive(Debug)]
1669pub struct ProcessorTopology {
1670 pub vp_count: u32,
1672 pub enable_smt: Option<bool>,
1674 pub vps_per_socket: Option<u32>,
1676 pub apic_mode: Option<ApicMode>,
1678}
1679
1680impl Default for ProcessorTopology {
1681 fn default() -> Self {
1682 Self {
1683 vp_count: 2,
1684 enable_smt: None,
1685 vps_per_socket: None,
1686 apic_mode: None,
1687 }
1688 }
1689}
1690
1691#[derive(Debug, Clone, Copy)]
1693pub enum ApicMode {
1694 Xapic,
1696 X2apicSupported,
1698 X2apicEnabled,
1700}
1701
1702#[derive(Debug)]
1704pub struct MemoryConfig {
1705 pub startup_bytes: u64,
1708 pub dynamic_memory_range: Option<(u64, u64)>,
1712}
1713
1714impl Default for MemoryConfig {
1715 fn default() -> Self {
1716 Self {
1717 startup_bytes: 0x1_0000_0000,
1718 dynamic_memory_range: None,
1719 }
1720 }
1721}
1722
1723#[derive(Debug)]
1725pub struct UefiConfig {
1726 pub secure_boot_enabled: bool,
1728 pub secure_boot_template: Option<SecureBootTemplate>,
1730 pub disable_frontpage: bool,
1732 pub default_boot_always_attempt: bool,
1734 pub enable_vpci_boot: bool,
1736}
1737
1738impl Default for UefiConfig {
1739 fn default() -> Self {
1740 Self {
1741 secure_boot_enabled: false,
1742 secure_boot_template: None,
1743 disable_frontpage: true,
1744 default_boot_always_attempt: false,
1745 enable_vpci_boot: false,
1746 }
1747 }
1748}
1749
1750#[derive(Debug, Clone)]
1752pub enum OpenvmmLogConfig {
1753 TestDefault,
1757 BuiltInDefault,
1760 Custom(BTreeMap<String, String>),
1770}
1771
1772#[derive(Debug)]
1774pub struct OpenHclConfig {
1775 pub vmbus_redirect: bool,
1777 pub custom_command_line: Option<String>,
1781 pub log_levels: OpenvmmLogConfig,
1785 pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
1788 pub vtl2_settings: Option<Vtl2Settings>,
1790}
1791
1792impl OpenHclConfig {
1793 pub fn command_line(&self) -> String {
1796 let mut cmdline = self.custom_command_line.clone();
1797
1798 append_cmdline(&mut cmdline, "OPENHCL_MANA_KEEP_ALIVE=host,privatepool");
1800
1801 match &self.log_levels {
1802 OpenvmmLogConfig::TestDefault => {
1803 let default_log_levels = {
1804 let openhcl_tracing = if let Ok(x) =
1806 std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG"))
1807 {
1808 format!("OPENVMM_LOG={x}")
1809 } else {
1810 "OPENVMM_LOG=debug".to_owned()
1811 };
1812 let openhcl_show_spans = if let Ok(x) = std::env::var("OPENVMM_SHOW_SPANS") {
1813 format!("OPENVMM_SHOW_SPANS={x}")
1814 } else {
1815 "OPENVMM_SHOW_SPANS=true".to_owned()
1816 };
1817 format!("{openhcl_tracing} {openhcl_show_spans}")
1818 };
1819 append_cmdline(&mut cmdline, &default_log_levels);
1820 }
1821 OpenvmmLogConfig::BuiltInDefault => {
1822 }
1824 OpenvmmLogConfig::Custom(levels) => {
1825 levels.iter().for_each(|(key, value)| {
1826 append_cmdline(&mut cmdline, format!("{key}={value}"));
1827 });
1828 }
1829 }
1830
1831 cmdline.unwrap_or_default()
1832 }
1833}
1834
1835impl Default for OpenHclConfig {
1836 fn default() -> Self {
1837 Self {
1838 vmbus_redirect: false,
1839 custom_command_line: None,
1840 log_levels: OpenvmmLogConfig::TestDefault,
1841 vtl2_base_address_type: None,
1842 vtl2_settings: None,
1843 }
1844 }
1845}
1846
1847#[derive(Debug)]
1849pub struct TpmConfig {
1850 pub no_persistent_secrets: bool,
1852}
1853
1854impl Default for TpmConfig {
1855 fn default() -> Self {
1856 Self {
1857 no_persistent_secrets: true,
1858 }
1859 }
1860}
1861
1862#[derive(Debug)]
1866pub enum Firmware {
1867 LinuxDirect {
1869 kernel: ResolvedArtifact,
1871 initrd: ResolvedArtifact,
1873 },
1874 OpenhclLinuxDirect {
1876 igvm_path: ResolvedArtifact,
1878 openhcl_config: OpenHclConfig,
1880 },
1881 Pcat {
1883 guest: PcatGuest,
1885 bios_firmware: ResolvedOptionalArtifact,
1887 svga_firmware: ResolvedOptionalArtifact,
1889 ide_controllers: [[Option<Drive>; 2]; 2],
1891 },
1892 OpenhclPcat {
1894 guest: PcatGuest,
1896 igvm_path: ResolvedArtifact,
1898 bios_firmware: ResolvedOptionalArtifact,
1900 svga_firmware: ResolvedOptionalArtifact,
1902 openhcl_config: OpenHclConfig,
1904 },
1905 Uefi {
1907 guest: UefiGuest,
1909 uefi_firmware: ResolvedArtifact,
1911 uefi_config: UefiConfig,
1913 },
1914 OpenhclUefi {
1916 guest: UefiGuest,
1918 isolation: Option<IsolationType>,
1920 igvm_path: ResolvedArtifact,
1922 uefi_config: UefiConfig,
1924 openhcl_config: OpenHclConfig,
1926 },
1927}
1928
1929#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1931pub enum BootDeviceType {
1932 None,
1934 Ide,
1936 IdeViaScsi,
1938 IdeViaNvme,
1940 Scsi,
1942 ScsiViaScsi,
1944 ScsiViaNvme,
1946 Nvme,
1948 NvmeViaScsi,
1950 NvmeViaNvme,
1952}
1953
1954impl BootDeviceType {
1955 fn requires_vtl2(&self) -> bool {
1956 match self {
1957 BootDeviceType::None
1958 | BootDeviceType::Ide
1959 | BootDeviceType::Scsi
1960 | BootDeviceType::Nvme => false,
1961 BootDeviceType::IdeViaScsi
1962 | BootDeviceType::IdeViaNvme
1963 | BootDeviceType::ScsiViaScsi
1964 | BootDeviceType::ScsiViaNvme
1965 | BootDeviceType::NvmeViaScsi
1966 | BootDeviceType::NvmeViaNvme => true,
1967 }
1968 }
1969
1970 fn requires_vpci_boot(&self) -> bool {
1971 matches!(
1972 self,
1973 BootDeviceType::Nvme | BootDeviceType::NvmeViaScsi | BootDeviceType::NvmeViaNvme
1974 )
1975 }
1976}
1977
1978impl Firmware {
1979 pub fn linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
1981 use petri_artifacts_vmm_test::artifacts::loadable::*;
1982 match arch {
1983 MachineArch::X86_64 => Firmware::LinuxDirect {
1984 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_X64).erase(),
1985 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
1986 },
1987 MachineArch::Aarch64 => Firmware::LinuxDirect {
1988 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_AARCH64).erase(),
1989 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_AARCH64).erase(),
1990 },
1991 }
1992 }
1993
1994 pub fn openhcl_linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
1996 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
1997 match arch {
1998 MachineArch::X86_64 => Firmware::OpenhclLinuxDirect {
1999 igvm_path: resolver.require(LATEST_LINUX_DIRECT_TEST_X64).erase(),
2000 openhcl_config: Default::default(),
2001 },
2002 MachineArch::Aarch64 => todo!("Linux direct not yet supported on aarch64"),
2003 }
2004 }
2005
2006 pub fn pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2008 use petri_artifacts_vmm_test::artifacts::loadable::*;
2009 Firmware::Pcat {
2010 guest,
2011 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2012 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2013 ide_controllers: [[None, None], [None, None]],
2014 }
2015 }
2016
2017 pub fn openhcl_pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2019 use petri_artifacts_vmm_test::artifacts::loadable::*;
2020 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2021 Firmware::OpenhclPcat {
2022 guest,
2023 igvm_path: resolver.require(LATEST_STANDARD_X64).erase(),
2024 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2025 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2026 openhcl_config: OpenHclConfig {
2027 vmbus_redirect: true,
2029 ..Default::default()
2030 },
2031 }
2032 }
2033
2034 pub fn uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch, guest: UefiGuest) -> Self {
2036 use petri_artifacts_vmm_test::artifacts::loadable::*;
2037 let uefi_firmware = match arch {
2038 MachineArch::X86_64 => resolver.require(UEFI_FIRMWARE_X64).erase(),
2039 MachineArch::Aarch64 => resolver.require(UEFI_FIRMWARE_AARCH64).erase(),
2040 };
2041 Firmware::Uefi {
2042 guest,
2043 uefi_firmware,
2044 uefi_config: Default::default(),
2045 }
2046 }
2047
2048 pub fn openhcl_uefi(
2050 resolver: &ArtifactResolver<'_>,
2051 arch: MachineArch,
2052 guest: UefiGuest,
2053 isolation: Option<IsolationType>,
2054 ) -> Self {
2055 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2056 let igvm_path = match arch {
2057 MachineArch::X86_64 if isolation.is_some() => resolver.require(LATEST_CVM_X64).erase(),
2058 MachineArch::X86_64 => resolver.require(LATEST_STANDARD_X64).erase(),
2059 MachineArch::Aarch64 => resolver.require(LATEST_STANDARD_AARCH64).erase(),
2060 };
2061 Firmware::OpenhclUefi {
2062 guest,
2063 isolation,
2064 igvm_path,
2065 uefi_config: Default::default(),
2066 openhcl_config: Default::default(),
2067 }
2068 }
2069
2070 fn is_openhcl(&self) -> bool {
2071 match self {
2072 Firmware::OpenhclLinuxDirect { .. }
2073 | Firmware::OpenhclUefi { .. }
2074 | Firmware::OpenhclPcat { .. } => true,
2075 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => false,
2076 }
2077 }
2078
2079 fn isolation(&self) -> Option<IsolationType> {
2080 match self {
2081 Firmware::OpenhclUefi { isolation, .. } => *isolation,
2082 Firmware::LinuxDirect { .. }
2083 | Firmware::Pcat { .. }
2084 | Firmware::Uefi { .. }
2085 | Firmware::OpenhclLinuxDirect { .. }
2086 | Firmware::OpenhclPcat { .. } => None,
2087 }
2088 }
2089
2090 fn is_linux_direct(&self) -> bool {
2091 match self {
2092 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => true,
2093 Firmware::Pcat { .. }
2094 | Firmware::Uefi { .. }
2095 | Firmware::OpenhclUefi { .. }
2096 | Firmware::OpenhclPcat { .. } => false,
2097 }
2098 }
2099
2100 fn is_pcat(&self) -> bool {
2101 match self {
2102 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => true,
2103 Firmware::Uefi { .. }
2104 | Firmware::OpenhclUefi { .. }
2105 | Firmware::LinuxDirect { .. }
2106 | Firmware::OpenhclLinuxDirect { .. } => false,
2107 }
2108 }
2109
2110 fn os_flavor(&self) -> OsFlavor {
2111 match self {
2112 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => OsFlavor::Linux,
2113 Firmware::Uefi {
2114 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2115 ..
2116 }
2117 | Firmware::OpenhclUefi {
2118 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2119 ..
2120 } => OsFlavor::Uefi,
2121 Firmware::Pcat {
2122 guest: PcatGuest::Vhd(cfg),
2123 ..
2124 }
2125 | Firmware::OpenhclPcat {
2126 guest: PcatGuest::Vhd(cfg),
2127 ..
2128 }
2129 | Firmware::Uefi {
2130 guest: UefiGuest::Vhd(cfg),
2131 ..
2132 }
2133 | Firmware::OpenhclUefi {
2134 guest: UefiGuest::Vhd(cfg),
2135 ..
2136 } => cfg.os_flavor,
2137 Firmware::Pcat {
2138 guest: PcatGuest::Iso(cfg),
2139 ..
2140 }
2141 | Firmware::OpenhclPcat {
2142 guest: PcatGuest::Iso(cfg),
2143 ..
2144 } => cfg.os_flavor,
2145 }
2146 }
2147
2148 fn quirks(&self) -> GuestQuirks {
2149 match self {
2150 Firmware::Pcat {
2151 guest: PcatGuest::Vhd(cfg),
2152 ..
2153 }
2154 | Firmware::Uefi {
2155 guest: UefiGuest::Vhd(cfg),
2156 ..
2157 }
2158 | Firmware::OpenhclUefi {
2159 guest: UefiGuest::Vhd(cfg),
2160 ..
2161 } => cfg.quirks.clone(),
2162 Firmware::Pcat {
2163 guest: PcatGuest::Iso(cfg),
2164 ..
2165 } => cfg.quirks.clone(),
2166 _ => Default::default(),
2167 }
2168 }
2169
2170 fn expected_boot_event(&self) -> Option<FirmwareEvent> {
2171 match self {
2172 Firmware::LinuxDirect { .. }
2173 | Firmware::OpenhclLinuxDirect { .. }
2174 | Firmware::Uefi {
2175 guest: UefiGuest::GuestTestUefi(_),
2176 ..
2177 }
2178 | Firmware::OpenhclUefi {
2179 guest: UefiGuest::GuestTestUefi(_),
2180 ..
2181 } => None,
2182 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => {
2183 Some(FirmwareEvent::BootAttempt)
2185 }
2186 Firmware::Uefi {
2187 guest: UefiGuest::None,
2188 ..
2189 }
2190 | Firmware::OpenhclUefi {
2191 guest: UefiGuest::None,
2192 ..
2193 } => Some(FirmwareEvent::NoBootDevice),
2194 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => {
2195 Some(FirmwareEvent::BootSuccess)
2196 }
2197 }
2198 }
2199
2200 fn openhcl_config(&self) -> Option<&OpenHclConfig> {
2201 match self {
2202 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2203 | Firmware::OpenhclUefi { openhcl_config, .. }
2204 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
2205 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2206 }
2207 }
2208
2209 fn openhcl_config_mut(&mut self) -> Option<&mut OpenHclConfig> {
2210 match self {
2211 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2212 | Firmware::OpenhclUefi { openhcl_config, .. }
2213 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
2214 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2215 }
2216 }
2217
2218 fn into_runtime_config(
2219 self,
2220 vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
2221 ) -> PetriVmRuntimeConfig {
2222 match self {
2223 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2224 | Firmware::OpenhclUefi { openhcl_config, .. }
2225 | Firmware::OpenhclPcat { openhcl_config, .. } => PetriVmRuntimeConfig {
2226 vtl2_settings: Some(
2227 openhcl_config
2228 .vtl2_settings
2229 .unwrap_or_else(default_vtl2_settings),
2230 ),
2231 ide_controllers: None,
2232 vmbus_storage_controllers,
2233 },
2234 Firmware::Pcat {
2235 ide_controllers, ..
2236 } => PetriVmRuntimeConfig {
2237 vtl2_settings: None,
2238 ide_controllers: Some(ide_controllers),
2239 vmbus_storage_controllers,
2240 },
2241 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } => PetriVmRuntimeConfig {
2242 vtl2_settings: None,
2243 ide_controllers: None,
2244 vmbus_storage_controllers,
2245 },
2246 }
2247 }
2248
2249 fn uefi_config(&self) -> Option<&UefiConfig> {
2250 match self {
2251 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
2252 Some(uefi_config)
2253 }
2254 Firmware::LinuxDirect { .. }
2255 | Firmware::OpenhclLinuxDirect { .. }
2256 | Firmware::Pcat { .. }
2257 | Firmware::OpenhclPcat { .. } => None,
2258 }
2259 }
2260
2261 fn uefi_config_mut(&mut self) -> Option<&mut UefiConfig> {
2262 match self {
2263 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
2264 Some(uefi_config)
2265 }
2266 Firmware::LinuxDirect { .. }
2267 | Firmware::OpenhclLinuxDirect { .. }
2268 | Firmware::Pcat { .. }
2269 | Firmware::OpenhclPcat { .. } => None,
2270 }
2271 }
2272
2273 fn boot_drive(&self) -> Option<Drive> {
2274 match self {
2275 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => None,
2276 Firmware::Pcat { guest, .. } | Firmware::OpenhclPcat { guest, .. } => {
2277 Some((guest.artifact().to_owned(), guest.is_dvd()))
2278 }
2279 Firmware::Uefi { guest, .. } | Firmware::OpenhclUefi { guest, .. } => {
2280 guest.artifact().map(|a| (a.to_owned(), false))
2281 }
2282 }
2283 .map(|(artifact, is_dvd)| {
2284 Drive::new(
2285 Some(Disk::Differencing(artifact.get().to_path_buf())),
2286 is_dvd,
2287 )
2288 })
2289 }
2290
2291 fn vtl2_settings(&mut self) -> Option<&mut Vtl2Settings> {
2292 self.openhcl_config_mut()
2293 .map(|c| c.vtl2_settings.get_or_insert_with(default_vtl2_settings))
2294 }
2295
2296 fn ide_controllers(&self) -> Option<&[[Option<Drive>; 2]; 2]> {
2297 match self {
2298 Firmware::Pcat {
2299 ide_controllers, ..
2300 } => Some(ide_controllers),
2301 _ => None,
2302 }
2303 }
2304
2305 fn ide_controllers_mut(&mut self) -> Option<&mut [[Option<Drive>; 2]; 2]> {
2306 match self {
2307 Firmware::Pcat {
2308 ide_controllers, ..
2309 } => Some(ide_controllers),
2310 _ => None,
2311 }
2312 }
2313}
2314
2315#[derive(Debug)]
2318pub enum PcatGuest {
2319 Vhd(BootImageConfig<boot_image_type::Vhd>),
2321 Iso(BootImageConfig<boot_image_type::Iso>),
2323}
2324
2325impl PcatGuest {
2326 fn artifact(&self) -> &ResolvedArtifact {
2327 match self {
2328 PcatGuest::Vhd(disk) => &disk.artifact,
2329 PcatGuest::Iso(disk) => &disk.artifact,
2330 }
2331 }
2332
2333 fn is_dvd(&self) -> bool {
2334 matches!(self, Self::Iso(_))
2335 }
2336}
2337
2338#[derive(Debug)]
2341pub enum UefiGuest {
2342 Vhd(BootImageConfig<boot_image_type::Vhd>),
2344 GuestTestUefi(ResolvedArtifact),
2346 None,
2348}
2349
2350impl UefiGuest {
2351 pub fn guest_test_uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2353 use petri_artifacts_vmm_test::artifacts::test_vhd::*;
2354 let artifact = match arch {
2355 MachineArch::X86_64 => resolver.require(GUEST_TEST_UEFI_X64).erase(),
2356 MachineArch::Aarch64 => resolver.require(GUEST_TEST_UEFI_AARCH64).erase(),
2357 };
2358 UefiGuest::GuestTestUefi(artifact)
2359 }
2360
2361 fn artifact(&self) -> Option<&ResolvedArtifact> {
2362 match self {
2363 UefiGuest::Vhd(vhd) => Some(&vhd.artifact),
2364 UefiGuest::GuestTestUefi(p) => Some(p),
2365 UefiGuest::None => None,
2366 }
2367 }
2368}
2369
2370pub mod boot_image_type {
2372 mod private {
2373 pub trait Sealed {}
2374 impl Sealed for super::Vhd {}
2375 impl Sealed for super::Iso {}
2376 }
2377
2378 pub trait BootImageType: private::Sealed {}
2381
2382 #[derive(Debug)]
2384 pub enum Vhd {}
2385
2386 #[derive(Debug)]
2388 pub enum Iso {}
2389
2390 impl BootImageType for Vhd {}
2391 impl BootImageType for Iso {}
2392}
2393
2394#[derive(Debug)]
2396pub struct BootImageConfig<T: boot_image_type::BootImageType> {
2397 artifact: ResolvedArtifact,
2399 os_flavor: OsFlavor,
2401 quirks: GuestQuirks,
2405 _type: core::marker::PhantomData<T>,
2407}
2408
2409impl BootImageConfig<boot_image_type::Vhd> {
2410 pub fn from_vhd<A>(artifact: ResolvedArtifact<A>) -> Self
2412 where
2413 A: petri_artifacts_common::tags::IsTestVhd,
2414 {
2415 BootImageConfig {
2416 artifact: artifact.erase(),
2417 os_flavor: A::OS_FLAVOR,
2418 quirks: A::quirks(),
2419 _type: std::marker::PhantomData,
2420 }
2421 }
2422}
2423
2424impl BootImageConfig<boot_image_type::Iso> {
2425 pub fn from_iso<A>(artifact: ResolvedArtifact<A>) -> Self
2427 where
2428 A: petri_artifacts_common::tags::IsTestIso,
2429 {
2430 BootImageConfig {
2431 artifact: artifact.erase(),
2432 os_flavor: A::OS_FLAVOR,
2433 quirks: A::quirks(),
2434 _type: std::marker::PhantomData,
2435 }
2436 }
2437}
2438
2439#[derive(Debug, Clone, Copy)]
2441pub enum IsolationType {
2442 Vbs,
2444 Snp,
2446 Tdx,
2448}
2449
2450#[derive(Debug, Clone, Copy)]
2452pub struct OpenHclServicingFlags {
2453 pub enable_nvme_keepalive: bool,
2456 pub enable_mana_keepalive: bool,
2458 pub override_version_checks: bool,
2460 pub stop_timeout_hint_secs: Option<u16>,
2462}
2463
2464#[derive(Debug, Clone)]
2466pub enum Disk {
2467 Memory(u64),
2469 Differencing(PathBuf),
2471 Persistent(PathBuf),
2473 Temporary(Arc<TempPath>),
2475}
2476
2477#[derive(Debug, Clone)]
2479pub struct PetriVmgsDisk {
2480 pub disk: Disk,
2482 pub encryption_policy: GuestStateEncryptionPolicy,
2484}
2485
2486impl Default for PetriVmgsDisk {
2487 fn default() -> Self {
2488 PetriVmgsDisk {
2489 disk: Disk::Memory(vmgs_format::VMGS_DEFAULT_CAPACITY),
2490 encryption_policy: GuestStateEncryptionPolicy::None(false),
2492 }
2493 }
2494}
2495
2496#[derive(Debug, Clone)]
2498pub enum PetriVmgsResource {
2499 Disk(PetriVmgsDisk),
2501 ReprovisionOnFailure(PetriVmgsDisk),
2503 Reprovision(PetriVmgsDisk),
2505 Ephemeral,
2507}
2508
2509impl PetriVmgsResource {
2510 pub fn disk(&self) -> Option<&PetriVmgsDisk> {
2512 match self {
2513 PetriVmgsResource::Disk(vmgs)
2514 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
2515 | PetriVmgsResource::Reprovision(vmgs) => Some(vmgs),
2516 PetriVmgsResource::Ephemeral => None,
2517 }
2518 }
2519}
2520
2521#[derive(Debug, Clone, Copy)]
2523pub enum PetriGuestStateLifetime {
2524 Disk,
2527 ReprovisionOnFailure,
2529 Reprovision,
2531 Ephemeral,
2533}
2534
2535#[derive(Debug, Clone, Copy)]
2537pub enum SecureBootTemplate {
2538 MicrosoftWindows,
2540 MicrosoftUefiCertificateAuthority,
2542}
2543
2544#[derive(Default, Debug, Clone)]
2547pub struct VmmQuirks {
2548 pub flaky_boot: Option<Duration>,
2551}
2552
2553fn make_vm_safe_name(name: &str) -> String {
2559 const MAX_VM_NAME_LENGTH: usize = 100;
2560 const HASH_LENGTH: usize = 4;
2561 const MAX_PREFIX_LENGTH: usize = MAX_VM_NAME_LENGTH - HASH_LENGTH;
2562
2563 if name.len() <= MAX_VM_NAME_LENGTH {
2564 name.to_owned()
2565 } else {
2566 let mut hasher = DefaultHasher::new();
2568 name.hash(&mut hasher);
2569 let hash = hasher.finish();
2570
2571 let hash_suffix = format!("{:04x}", hash & 0xFFFF);
2573
2574 let truncated = &name[..MAX_PREFIX_LENGTH];
2576 tracing::debug!(
2577 "VM name too long ({}), truncating '{}' to '{}{}'",
2578 name.len(),
2579 name,
2580 truncated,
2581 hash_suffix
2582 );
2583
2584 format!("{}{}", truncated, hash_suffix)
2585 }
2586}
2587
2588#[derive(Debug, Clone, Copy, Eq, PartialEq)]
2590pub enum PetriHaltReason {
2591 PowerOff,
2593 Reset,
2595 Hibernate,
2597 TripleFault,
2599 Other,
2601}
2602
2603fn append_cmdline(cmd: &mut Option<String>, add_cmd: impl AsRef<str>) {
2604 if let Some(cmd) = cmd.as_mut() {
2605 cmd.push(' ');
2606 cmd.push_str(add_cmd.as_ref());
2607 } else {
2608 *cmd = Some(add_cmd.as_ref().to_string());
2609 }
2610}
2611
2612async fn save_inspect(
2613 name: &str,
2614 inspect: std::pin::Pin<Box<dyn Future<Output = anyhow::Result<inspect::Node>> + Send>>,
2615 log_source: &PetriLogSource,
2616) {
2617 tracing::info!("Collecting {name} inspect details.");
2618 let node = match inspect.await {
2619 Ok(n) => n,
2620 Err(e) => {
2621 tracing::error!(?e, "Failed to get {name}");
2622 return;
2623 }
2624 };
2625 if let Err(e) = log_source.write_attachment(
2626 &format!("timeout_inspect_{name}.log"),
2627 format!("{node:#}").as_bytes(),
2628 ) {
2629 tracing::error!(?e, "Failed to save {name} inspect log");
2630 return;
2631 }
2632 tracing::info!("{name} inspect task finished.");
2633}
2634
2635pub struct ModifyFn<T>(pub Box<dyn FnOnce(T) -> T + Send>);
2637
2638impl<T> Debug for ModifyFn<T> {
2639 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2640 write!(f, "_")
2641 }
2642}
2643
2644fn default_vtl2_settings() -> Vtl2Settings {
2646 Vtl2Settings {
2647 version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
2648 fixed: None,
2649 dynamic: Some(Default::default()),
2650 namespace_settings: Default::default(),
2651 }
2652}
2653
2654#[derive(Debug, Copy, Clone, PartialEq, Eq)]
2656pub enum Vtl {
2657 Vtl0 = 0,
2659 Vtl1 = 1,
2661 Vtl2 = 2,
2663}
2664
2665#[derive(Debug, Copy, Clone, PartialEq, Eq)]
2667pub enum VmbusStorageType {
2668 Scsi,
2670 Nvme,
2672}
2673
2674#[derive(Debug, Clone)]
2676pub struct Drive {
2677 pub disk: Option<Disk>,
2679 pub is_dvd: bool,
2681}
2682
2683impl Drive {
2684 pub fn new(disk: Option<Disk>, is_dvd: bool) -> Self {
2686 Self { disk, is_dvd }
2687 }
2688}
2689
2690#[derive(Debug, Clone)]
2692pub struct VmbusStorageController {
2693 pub target_vtl: Vtl,
2695 pub controller_type: VmbusStorageType,
2697 pub drives: HashMap<u32, Drive>,
2699}
2700
2701impl VmbusStorageController {
2702 pub fn new(target_vtl: Vtl, controller_type: VmbusStorageType) -> Self {
2704 Self {
2705 target_vtl,
2706 controller_type,
2707 drives: HashMap::new(),
2708 }
2709 }
2710
2711 pub fn set_drive(
2713 &mut self,
2714 lun: Option<u32>,
2715 drive: Drive,
2716 allow_modify_existing: bool,
2717 ) -> u32 {
2718 let lun = lun.unwrap_or_else(|| {
2719 let mut lun = None;
2721 for x in 0..u8::MAX as u32 {
2722 if !self.drives.contains_key(&x) {
2723 lun = Some(x);
2724 break;
2725 }
2726 }
2727 lun.expect("all locations on this controller are in use")
2728 });
2729
2730 if self.drives.insert(lun, drive).is_some() && !allow_modify_existing {
2731 panic!("a disk with lun {lun} already existed on this controller");
2732 }
2733
2734 lun
2735 }
2736}
2737
2738#[cfg(test)]
2739mod tests {
2740 use super::make_vm_safe_name;
2741 use crate::Drive;
2742 use crate::VmbusStorageController;
2743 use crate::VmbusStorageType;
2744 use crate::Vtl;
2745
2746 #[test]
2747 fn test_short_names_unchanged() {
2748 let short_name = "short_test_name";
2749 assert_eq!(make_vm_safe_name(short_name), short_name);
2750 }
2751
2752 #[test]
2753 fn test_exactly_100_chars_unchanged() {
2754 let name_100 = "a".repeat(100);
2755 assert_eq!(make_vm_safe_name(&name_100), name_100);
2756 }
2757
2758 #[test]
2759 fn test_long_name_truncated() {
2760 let long_name = "multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_openhcl_servicing";
2761 let result = make_vm_safe_name(long_name);
2762
2763 assert_eq!(result.len(), 100);
2765
2766 assert!(result.starts_with("multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_ope"));
2768
2769 let suffix = &result[96..];
2771 assert_eq!(suffix.len(), 4);
2772 assert!(u16::from_str_radix(suffix, 16).is_ok());
2774 }
2775
2776 #[test]
2777 fn test_deterministic_results() {
2778 let long_name = "very_long_test_name_that_exceeds_the_100_character_limit_and_should_be_truncated_consistently_every_time";
2779 let result1 = make_vm_safe_name(long_name);
2780 let result2 = make_vm_safe_name(long_name);
2781
2782 assert_eq!(result1, result2);
2783 assert_eq!(result1.len(), 100);
2784 }
2785
2786 #[test]
2787 fn test_different_names_different_hashes() {
2788 let name1 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_1";
2789 let name2 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_2";
2790
2791 let result1 = make_vm_safe_name(name1);
2792 let result2 = make_vm_safe_name(name2);
2793
2794 assert_eq!(result1.len(), 100);
2796 assert_eq!(result2.len(), 100);
2797
2798 assert_ne!(result1, result2);
2800 assert_ne!(&result1[96..], &result2[96..]);
2801 }
2802
2803 #[test]
2804 fn test_vmbus_storage_controller() {
2805 let mut controller = VmbusStorageController::new(Vtl::Vtl0, VmbusStorageType::Scsi);
2806 assert_eq!(
2807 controller.set_drive(Some(1), Drive::new(None, false), false),
2808 1
2809 );
2810 assert!(controller.drives.contains_key(&1));
2811 assert_eq!(
2812 controller.set_drive(None, Drive::new(None, false), false),
2813 0
2814 );
2815 assert!(controller.drives.contains_key(&0));
2816 assert_eq!(
2817 controller.set_drive(None, Drive::new(None, false), false),
2818 2
2819 );
2820 assert!(controller.drives.contains_key(&2));
2821 assert_eq!(
2822 controller.set_drive(Some(0), Drive::new(None, false), true),
2823 0
2824 );
2825 assert!(controller.drives.contains_key(&0));
2826 }
2827}