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