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 pub pipette_binary: Option<ResolvedArtifact>,
73}
74
75impl<T: PetriVmmBackend> PetriVmArtifacts<T> {
76 pub fn new(
80 resolver: &ArtifactResolver<'_>,
81 firmware: Firmware,
82 arch: MachineArch,
83 with_vtl0_pipette: bool,
84 ) -> Option<Self> {
85 if !T::check_compat(&firmware, arch) {
86 return None;
87 }
88
89 let pipette_binary = if with_vtl0_pipette {
90 Some(Self::resolve_pipette_binary(
91 resolver,
92 firmware.os_flavor(),
93 arch,
94 ))
95 } else {
96 None
97 };
98
99 Some(Self {
100 backend: T::new(resolver),
101 arch,
102 agent_image: Some(if with_vtl0_pipette {
103 AgentImage::new(firmware.os_flavor()).with_pipette(resolver, arch)
104 } else {
105 AgentImage::new(firmware.os_flavor())
106 }),
107 openhcl_agent_image: if firmware.is_openhcl() {
108 Some(AgentImage::new(OsFlavor::Linux).with_pipette(resolver, arch))
109 } else {
110 None
111 },
112 pipette_binary,
113 firmware,
114 })
115 }
116
117 fn resolve_pipette_binary(
118 resolver: &ArtifactResolver<'_>,
119 os_flavor: OsFlavor,
120 arch: MachineArch,
121 ) -> ResolvedArtifact {
122 use petri_artifacts_common::artifacts as common_artifacts;
123 match (os_flavor, arch) {
124 (OsFlavor::Linux, MachineArch::X86_64) => resolver
125 .require(common_artifacts::PIPETTE_LINUX_X64)
126 .erase(),
127 (OsFlavor::Linux, MachineArch::Aarch64) => resolver
128 .require(common_artifacts::PIPETTE_LINUX_AARCH64)
129 .erase(),
130 (OsFlavor::Windows, MachineArch::X86_64) => resolver
131 .require(common_artifacts::PIPETTE_WINDOWS_X64)
132 .erase(),
133 (OsFlavor::Windows, MachineArch::Aarch64) => resolver
134 .require(common_artifacts::PIPETTE_WINDOWS_AARCH64)
135 .erase(),
136 (OsFlavor::FreeBsd | OsFlavor::Uefi, _) => {
137 panic!("No pipette binary for this OS flavor")
138 }
139 }
140 }
141}
142
143pub struct PetriVmBuilder<T: PetriVmmBackend> {
145 backend: T,
147 config: PetriVmConfig,
149 modify_vmm_config: Option<ModifyFn<T::VmmConfig>>,
151 resources: PetriVmResources,
153
154 guest_quirks: GuestQuirksInner,
156 vmm_quirks: VmmQuirks,
157
158 expected_boot_event: Option<FirmwareEvent>,
161 override_expect_reset: bool,
162
163 agent_image: Option<AgentImage>,
167 openhcl_agent_image: Option<AgentImage>,
169 boot_device_type: BootDeviceType,
171
172 minimal_mode: bool,
174 pipette_binary: Option<ResolvedArtifact>,
176 enable_serial: bool,
178 enable_screenshots: bool,
180 prebuilt_initrd: Option<PathBuf>,
182}
183
184impl<T: PetriVmmBackend> Debug for PetriVmBuilder<T> {
185 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186 f.debug_struct("PetriVmBuilder")
187 .field("backend", &self.backend)
188 .field("config", &self.config)
189 .field("modify_vmm_config", &self.modify_vmm_config.is_some())
190 .field("resources", &self.resources)
191 .field("guest_quirks", &self.guest_quirks)
192 .field("vmm_quirks", &self.vmm_quirks)
193 .field("expected_boot_event", &self.expected_boot_event)
194 .field("override_expect_reset", &self.override_expect_reset)
195 .field("agent_image", &self.agent_image)
196 .field("openhcl_agent_image", &self.openhcl_agent_image)
197 .field("boot_device_type", &self.boot_device_type)
198 .field("minimal_mode", &self.minimal_mode)
199 .field("enable_serial", &self.enable_serial)
200 .field("enable_screenshots", &self.enable_screenshots)
201 .field("prebuilt_initrd", &self.prebuilt_initrd)
202 .finish()
203 }
204}
205
206#[derive(Debug)]
208pub struct PetriVmConfig {
209 pub name: String,
211 pub arch: MachineArch,
213 pub host_log_levels: Option<OpenvmmLogConfig>,
215 pub firmware: Firmware,
217 pub memory: MemoryConfig,
219 pub proc_topology: ProcessorTopology,
221 pub vmgs: PetriVmgsResource,
223 pub tpm: Option<TpmConfig>,
225 pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
227}
228
229pub struct PetriVmProperties {
232 pub is_openhcl: bool,
234 pub is_isolated: bool,
236 pub is_pcat: bool,
238 pub is_linux_direct: bool,
240 pub using_vtl0_pipette: bool,
242 pub using_vpci: bool,
244 pub os_flavor: OsFlavor,
246 pub minimal_mode: bool,
248 pub uses_pipette_as_init: bool,
250 pub enable_serial: bool,
252 pub prebuilt_initrd: Option<PathBuf>,
254 pub has_agent_disk: bool,
256}
257
258pub struct PetriVmRuntimeConfig {
260 pub vtl2_settings: Option<Vtl2Settings>,
262 pub ide_controllers: Option<[[Option<Drive>; 2]; 2]>,
264 pub vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
266}
267
268#[derive(Debug)]
270pub struct PetriVmResources {
271 driver: DefaultDriver,
272 log_source: PetriLogSource,
273}
274
275#[async_trait]
277pub trait PetriVmmBackend: Debug {
278 type VmmConfig;
280
281 type VmRuntime: PetriVmRuntime;
283
284 fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool;
287
288 fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks);
290
291 fn default_servicing_flags() -> OpenHclServicingFlags;
293
294 fn create_guest_dump_disk() -> anyhow::Result<
297 Option<(
298 Arc<TempPath>,
299 Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
300 )>,
301 >;
302
303 fn new(resolver: &ArtifactResolver<'_>) -> Self;
305
306 async fn run(
308 self,
309 config: PetriVmConfig,
310 modify_vmm_config: Option<ModifyFn<Self::VmmConfig>>,
311 resources: &PetriVmResources,
312 properties: PetriVmProperties,
313 ) -> anyhow::Result<(Self::VmRuntime, PetriVmRuntimeConfig)>;
314}
315
316pub(crate) const PETRI_IDE_BOOT_CONTROLLER_NUMBER: u32 = 0;
318pub(crate) const PETRI_IDE_BOOT_LUN: u8 = 0;
319pub(crate) const PETRI_IDE_BOOT_CONTROLLER: Guid =
320 guid::guid!("ca56751f-e643-4bef-bf54-f73678e8b7b5");
321
322pub(crate) const PETRI_SCSI_BOOT_LUN: u32 = 0;
324pub(crate) const PETRI_SCSI_PIPETTE_LUN: u32 = 1;
325pub(crate) const PETRI_SCSI_CRASH_LUN: u32 = 2;
326pub(crate) const PETRI_SCSI_VTL0_CONTROLLER: Guid =
328 guid::guid!("27b553e8-8b39-411b-a55f-839971a7884f");
329pub(crate) const PETRI_SCSI_VTL2_CONTROLLER: Guid =
331 guid::guid!("766e96f8-2ceb-437e-afe3-a93169e48a7c");
332pub(crate) const PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER: Guid =
334 guid::guid!("6c474f47-ed39-49e6-bbb9-142177a1da6e");
335
336pub(crate) const PETRI_NVME_BOOT_NSID: u32 = 37;
338pub(crate) const PETRI_NVME_BOOT_VTL0_CONTROLLER: Guid =
340 guid::guid!("e23a04e2-90f5-4852-bc9d-e7ac691b756c");
341pub(crate) const PETRI_NVME_BOOT_VTL2_CONTROLLER: Guid =
343 guid::guid!("92bc8346-718b-449a-8751-edbf3dcd27e4");
344
345pub struct PetriVm<T: PetriVmmBackend> {
347 resources: PetriVmResources,
348 runtime: T::VmRuntime,
349 watchdog_tasks: Vec<Task<()>>,
350 openhcl_diag_handler: Option<OpenHclDiagHandler>,
351
352 arch: MachineArch,
353 guest_quirks: GuestQuirksInner,
354 vmm_quirks: VmmQuirks,
355 expected_boot_event: Option<FirmwareEvent>,
356 uses_pipette_as_init: bool,
357
358 config: PetriVmRuntimeConfig,
359}
360
361impl<T: PetriVmmBackend> PetriVmBuilder<T> {
362 pub fn new(
364 params: PetriTestParams<'_>,
365 artifacts: PetriVmArtifacts<T>,
366 driver: &DefaultDriver,
367 ) -> anyhow::Result<Self> {
368 let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
369 let expected_boot_event = artifacts.firmware.expected_boot_event();
370 let boot_device_type = match artifacts.firmware {
371 Firmware::LinuxDirect { .. } => BootDeviceType::None,
372 Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
373 Firmware::Pcat { .. } => BootDeviceType::Ide,
374 Firmware::OpenhclPcat { .. } => BootDeviceType::IdeViaScsi,
375 Firmware::Uefi {
376 guest: UefiGuest::None,
377 ..
378 }
379 | Firmware::OpenhclUefi {
380 guest: UefiGuest::None,
381 ..
382 } => BootDeviceType::None,
383 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
384 };
385
386 Ok(Self {
387 backend: artifacts.backend,
388 config: PetriVmConfig {
389 name: make_vm_safe_name(params.test_name),
390 arch: artifacts.arch,
391 host_log_levels: None,
392 firmware: artifacts.firmware,
393 memory: Default::default(),
394 proc_topology: Default::default(),
395
396 vmgs: PetriVmgsResource::Ephemeral,
397 tpm: None,
398 vmbus_storage_controllers: HashMap::new(),
399 },
400 modify_vmm_config: None,
401 resources: PetriVmResources {
402 driver: driver.clone(),
403 log_source: params.logger.clone(),
404 },
405
406 guest_quirks,
407 vmm_quirks,
408 expected_boot_event,
409 override_expect_reset: false,
410
411 agent_image: artifacts.agent_image,
412 openhcl_agent_image: artifacts.openhcl_agent_image,
413 boot_device_type,
414
415 minimal_mode: false,
416 pipette_binary: artifacts.pipette_binary,
417 enable_serial: true,
418 enable_screenshots: true,
419 prebuilt_initrd: None,
420 }
421 .add_petri_scsi_controllers()
422 .add_guest_crash_disk(params.post_test_hooks))
423 }
424
425 pub fn minimal(
436 params: PetriTestParams<'_>,
437 artifacts: PetriVmArtifacts<T>,
438 driver: &DefaultDriver,
439 ) -> anyhow::Result<Self> {
440 let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
441 let expected_boot_event = artifacts.firmware.expected_boot_event();
442 let boot_device_type = match artifacts.firmware {
443 Firmware::LinuxDirect { .. } => BootDeviceType::None,
444 Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
445 Firmware::Pcat { .. } => BootDeviceType::Ide,
446 Firmware::OpenhclPcat { .. } => BootDeviceType::IdeViaScsi,
447 Firmware::Uefi {
448 guest: UefiGuest::None,
449 ..
450 }
451 | Firmware::OpenhclUefi {
452 guest: UefiGuest::None,
453 ..
454 } => BootDeviceType::None,
455 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
456 };
457
458 Ok(Self {
459 backend: artifacts.backend,
460 config: PetriVmConfig {
461 name: make_vm_safe_name(params.test_name),
462 arch: artifacts.arch,
463 host_log_levels: None,
464 firmware: artifacts.firmware,
465 memory: Default::default(),
466 proc_topology: Default::default(),
467
468 vmgs: PetriVmgsResource::Ephemeral,
469 tpm: None,
470 vmbus_storage_controllers: HashMap::new(),
471 },
472 modify_vmm_config: None,
473 resources: PetriVmResources {
474 driver: driver.clone(),
475 log_source: params.logger.clone(),
476 },
477
478 guest_quirks,
479 vmm_quirks,
480 expected_boot_event,
481 override_expect_reset: false,
482
483 agent_image: artifacts.agent_image,
484 openhcl_agent_image: artifacts.openhcl_agent_image,
485 boot_device_type,
486
487 minimal_mode: true,
488 pipette_binary: artifacts.pipette_binary,
489 enable_serial: false,
490 enable_screenshots: true,
491 prebuilt_initrd: None,
492 })
493 }
494
495 pub fn is_minimal(&self) -> bool {
497 self.minimal_mode
498 }
499
500 pub fn with_prebuilt_initrd(mut self, path: PathBuf) -> Self {
507 self.prebuilt_initrd = Some(path);
508 self
509 }
510
511 pub fn prepare_initrd(&self) -> anyhow::Result<TempPath> {
522 use anyhow::Context;
523 use std::io::Write;
524
525 let initrd_path = self
526 .config
527 .firmware
528 .linux_direct_initrd()
529 .context("prepare_initrd requires Linux direct boot with initrd")?;
530 let pipette_path = self
531 .pipette_binary
532 .as_ref()
533 .context("prepare_initrd requires a pipette binary")?;
534
535 let initrd_gz = std::fs::read(initrd_path)
536 .with_context(|| format!("failed to read initrd at {}", initrd_path.display()))?;
537 let pipette_data = std::fs::read(pipette_path.get()).with_context(|| {
538 format!(
539 "failed to read pipette binary at {}",
540 pipette_path.get().display()
541 )
542 })?;
543
544 let merged_gz =
545 crate::cpio::inject_into_initrd(&initrd_gz, "pipette", &pipette_data, 0o100755)
546 .context("failed to inject pipette into initrd")?;
547
548 let mut tmp = tempfile::NamedTempFile::new()
549 .context("failed to create temp file for pre-built initrd")?;
550 tmp.write_all(&merged_gz)
551 .context("failed to write pre-built initrd")?;
552
553 Ok(tmp.into_temp_path())
554 }
555
556 pub fn with_serial_output(mut self) -> Self {
565 self.enable_serial = true;
566 self
567 }
568
569 pub fn without_serial_output(mut self) -> Self {
574 self.enable_serial = false;
575 self
576 }
577
578 pub fn without_screenshots(mut self) -> Self {
583 self.enable_screenshots = false;
584 self
585 }
586
587 fn add_petri_scsi_controllers(self) -> Self {
588 let builder = self.add_vmbus_storage_controller(
589 &PETRI_SCSI_VTL0_CONTROLLER,
590 Vtl::Vtl0,
591 VmbusStorageType::Scsi,
592 );
593
594 if builder.is_openhcl() {
595 builder.add_vmbus_storage_controller(
596 &PETRI_SCSI_VTL2_CONTROLLER,
597 Vtl::Vtl2,
598 VmbusStorageType::Scsi,
599 )
600 } else {
601 builder
602 }
603 }
604
605 fn add_guest_crash_disk(self, post_test_hooks: &mut Vec<PetriPostTestHook>) -> Self {
606 let logger = self.resources.log_source.clone();
607 let (disk, disk_hook) = matches!(
608 self.config.firmware.os_flavor(),
609 OsFlavor::Windows | OsFlavor::Linux
610 )
611 .then(|| T::create_guest_dump_disk().expect("failed to create guest dump disk"))
612 .flatten()
613 .unzip();
614
615 if let Some(disk_hook) = disk_hook {
616 post_test_hooks.push(PetriPostTestHook::new(
617 "extract guest crash dumps".into(),
618 move |test_passed| {
619 if test_passed {
620 return Ok(());
621 }
622 let mut disk = disk_hook()?;
623 let gpt = gptman::GPT::read_from(&mut disk, SECTOR_SIZE)?;
624 let partition = fscommon::StreamSlice::new(
625 &mut disk,
626 gpt[1].starting_lba * SECTOR_SIZE,
627 gpt[1].ending_lba * SECTOR_SIZE,
628 )?;
629 let fs = fatfs::FileSystem::new(partition, fatfs::FsOptions::new())?;
630 for entry in fs.root_dir().iter() {
631 let Ok(entry) = entry else {
632 tracing::warn!(?entry, "failed to read entry in guest crash dump disk");
633 continue;
634 };
635 if !entry.is_file() {
636 tracing::warn!(
637 ?entry,
638 "skipping non-file entry in guest crash dump disk"
639 );
640 continue;
641 }
642 logger.write_attachment(&entry.file_name(), entry.to_file())?;
643 }
644 Ok(())
645 },
646 ));
647 }
648
649 if let Some(disk) = disk {
650 self.add_vmbus_drive(
651 Drive::new(Some(Disk::Temporary(disk)), false),
652 &PETRI_SCSI_VTL0_CONTROLLER,
653 Some(PETRI_SCSI_CRASH_LUN),
654 )
655 } else {
656 self
657 }
658 }
659
660 fn add_agent_disks(self) -> Self {
661 self.add_agent_disk_inner(Vtl::Vtl0)
662 .add_agent_disk_inner(Vtl::Vtl2)
663 }
664
665 fn add_agent_disk_inner(mut self, target_vtl: Vtl) -> Self {
666 let (agent_image, controller_id) = match target_vtl {
667 Vtl::Vtl0 => (self.agent_image.as_ref(), PETRI_SCSI_VTL0_CONTROLLER),
668 Vtl::Vtl1 => panic!("no VTL1 agent disk"),
669 Vtl::Vtl2 => (
670 self.openhcl_agent_image.as_ref(),
671 PETRI_SCSI_VTL2_CONTROLLER,
672 ),
673 };
674
675 if target_vtl == Vtl::Vtl0
678 && self.uses_pipette_as_init()
679 && !agent_image.is_some_and(|i| i.has_extras())
680 {
681 return self;
682 }
683
684 let Some(agent_disk) = agent_image.and_then(|i| {
685 i.build(crate::disk_image::ImageType::Vhd)
686 .expect("failed to build agent image")
687 }) else {
688 return self;
689 };
690
691 if !self
694 .config
695 .vmbus_storage_controllers
696 .contains_key(&controller_id)
697 {
698 self = self.add_vmbus_storage_controller(
699 &controller_id,
700 target_vtl,
701 VmbusStorageType::Scsi,
702 );
703 }
704
705 self.add_vmbus_drive(
706 Drive::new(
707 Some(Disk::Temporary(Arc::new(agent_disk.into_temp_path()))),
708 false,
709 ),
710 &controller_id,
711 Some(PETRI_SCSI_PIPETTE_LUN),
712 )
713 }
714
715 fn add_boot_disk(mut self) -> Self {
716 if self.boot_device_type.requires_vtl2() && !self.is_openhcl() {
717 panic!("boot device type {:?} requires vtl2", self.boot_device_type);
718 }
719
720 if self.boot_device_type.requires_vpci_boot() {
721 self.config
722 .firmware
723 .uefi_config_mut()
724 .expect("vpci boot requires uefi")
725 .enable_vpci_boot = true;
726 }
727
728 if let Some(boot_drive) = self.config.firmware.boot_drive() {
729 match self.boot_device_type {
730 BootDeviceType::None => unreachable!(),
731 BootDeviceType::Ide => self.add_ide_drive(
732 boot_drive,
733 PETRI_IDE_BOOT_CONTROLLER_NUMBER,
734 PETRI_IDE_BOOT_LUN,
735 ),
736 BootDeviceType::IdeViaScsi => self
737 .add_vmbus_drive(
738 boot_drive,
739 &PETRI_SCSI_VTL2_CONTROLLER,
740 Some(PETRI_SCSI_BOOT_LUN),
741 )
742 .add_vtl2_storage_controller(
743 Vtl2StorageControllerBuilder::new(ControllerType::Ide)
744 .with_instance_id(PETRI_IDE_BOOT_CONTROLLER)
745 .add_lun(
746 Vtl2LunBuilder::disk()
747 .with_channel(PETRI_IDE_BOOT_CONTROLLER_NUMBER)
748 .with_location(PETRI_IDE_BOOT_LUN as u32)
749 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
750 ControllerType::Scsi,
751 PETRI_SCSI_VTL2_CONTROLLER,
752 PETRI_SCSI_BOOT_LUN,
753 )),
754 )
755 .build(),
756 ),
757 BootDeviceType::IdeViaNvme => todo!(),
758 BootDeviceType::Scsi => self.add_vmbus_drive(
759 boot_drive,
760 &PETRI_SCSI_VTL0_CONTROLLER,
761 Some(PETRI_SCSI_BOOT_LUN),
762 ),
763 BootDeviceType::ScsiViaScsi => self
764 .add_vmbus_drive(
765 boot_drive,
766 &PETRI_SCSI_VTL2_CONTROLLER,
767 Some(PETRI_SCSI_BOOT_LUN),
768 )
769 .add_vtl2_storage_controller(
770 Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
771 .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
772 .add_lun(
773 Vtl2LunBuilder::disk()
774 .with_location(PETRI_SCSI_BOOT_LUN)
775 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
776 ControllerType::Scsi,
777 PETRI_SCSI_VTL2_CONTROLLER,
778 PETRI_SCSI_BOOT_LUN,
779 )),
780 )
781 .build(),
782 ),
783 BootDeviceType::ScsiViaNvme => self
784 .add_vmbus_storage_controller(
785 &PETRI_NVME_BOOT_VTL2_CONTROLLER,
786 Vtl::Vtl2,
787 VmbusStorageType::Nvme,
788 )
789 .add_vmbus_drive(
790 boot_drive,
791 &PETRI_NVME_BOOT_VTL2_CONTROLLER,
792 Some(PETRI_NVME_BOOT_NSID),
793 )
794 .add_vtl2_storage_controller(
795 Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
796 .with_instance_id(PETRI_SCSI_VTL0_VIA_VTL2_CONTROLLER)
797 .add_lun(
798 Vtl2LunBuilder::disk()
799 .with_location(PETRI_SCSI_BOOT_LUN)
800 .with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
801 ControllerType::Nvme,
802 PETRI_NVME_BOOT_VTL2_CONTROLLER,
803 PETRI_NVME_BOOT_NSID,
804 )),
805 )
806 .build(),
807 ),
808 BootDeviceType::Nvme => self
809 .add_vmbus_storage_controller(
810 &PETRI_NVME_BOOT_VTL0_CONTROLLER,
811 Vtl::Vtl0,
812 VmbusStorageType::Nvme,
813 )
814 .add_vmbus_drive(
815 boot_drive,
816 &PETRI_NVME_BOOT_VTL0_CONTROLLER,
817 Some(PETRI_NVME_BOOT_NSID),
818 ),
819 BootDeviceType::NvmeViaScsi => todo!(),
820 BootDeviceType::NvmeViaNvme => todo!(),
821 }
822 } else {
823 self
824 }
825 }
826
827 fn has_agent_disk(&self) -> bool {
832 if self.uses_pipette_as_init() {
833 self.agent_image.as_ref().is_some_and(|i| i.has_extras())
834 } else {
835 self.agent_image.is_some()
836 }
837 }
838
839 pub fn properties(&self) -> PetriVmProperties {
841 PetriVmProperties {
842 is_openhcl: self.config.firmware.is_openhcl(),
843 is_isolated: self.config.firmware.isolation().is_some(),
844 is_pcat: self.config.firmware.is_pcat(),
845 is_linux_direct: self.config.firmware.is_linux_direct(),
846 using_vtl0_pipette: self.using_vtl0_pipette(),
847 using_vpci: self.boot_device_type.requires_vpci_boot(),
848 os_flavor: self.config.firmware.os_flavor(),
849 minimal_mode: self.minimal_mode,
850 uses_pipette_as_init: self.uses_pipette_as_init(),
851 enable_serial: self.enable_serial,
852 prebuilt_initrd: self.prebuilt_initrd.clone(),
853 has_agent_disk: self.has_agent_disk(),
854 }
855 }
856
857 fn uses_pipette_as_init(&self) -> bool {
863 self.config.firmware.is_linux_direct()
864 && !self.config.firmware.is_openhcl()
865 && self.pipette_binary.is_some()
866 }
867
868 pub fn using_vtl0_pipette(&self) -> bool {
870 self.uses_pipette_as_init()
871 || self
872 .agent_image
873 .as_ref()
874 .is_some_and(|x| x.contains_pipette())
875 }
876
877 pub async fn run_without_agent(self) -> anyhow::Result<PetriVm<T>> {
881 self.run_core().await
882 }
883
884 pub async fn run(self) -> anyhow::Result<(PetriVm<T>, PipetteClient)> {
887 assert!(self.using_vtl0_pipette());
888
889 let mut vm = self.run_core().await?;
890 let client = vm.wait_for_agent().await?;
891 Ok((vm, client))
892 }
893
894 async fn run_core(mut self) -> anyhow::Result<PetriVm<T>> {
895 self = self.add_boot_disk().add_agent_disks();
898
899 let _prepared_initrd_guard;
903 if self.uses_pipette_as_init() && self.prebuilt_initrd.is_none() {
904 let tmp = self.prepare_initrd()?;
905 self.prebuilt_initrd = Some(tmp.to_path_buf());
906 _prepared_initrd_guard = Some(tmp);
907 } else {
908 _prepared_initrd_guard = None;
909 }
910
911 tracing::debug!(builder = ?self);
912
913 let arch = self.config.arch;
914 let expect_reset = self.expect_reset();
915 let uses_pipette_as_init = self.uses_pipette_as_init();
916 let properties = self.properties();
917
918 let (mut runtime, config) = self
919 .backend
920 .run(
921 self.config,
922 self.modify_vmm_config,
923 &self.resources,
924 properties,
925 )
926 .await?;
927 let openhcl_diag_handler = runtime.openhcl_diag();
928 let watchdog_tasks =
929 Self::start_watchdog_tasks(&self.resources, &mut runtime, self.enable_screenshots)?;
930
931 let mut vm = PetriVm {
932 resources: self.resources,
933 runtime,
934 watchdog_tasks,
935 openhcl_diag_handler,
936
937 arch,
938 guest_quirks: self.guest_quirks,
939 vmm_quirks: self.vmm_quirks,
940 expected_boot_event: self.expected_boot_event,
941 uses_pipette_as_init,
942
943 config,
944 };
945
946 if expect_reset {
947 vm.wait_for_reset_core().await?;
948 }
949
950 vm.wait_for_expected_boot_event().await?;
951
952 Ok(vm)
953 }
954
955 fn expect_reset(&self) -> bool {
956 self.override_expect_reset
957 || matches!(
958 (
959 self.guest_quirks.initial_reboot,
960 self.expected_boot_event,
961 &self.config.firmware,
962 &self.config.tpm,
963 ),
964 (
965 Some(InitialRebootCondition::Always),
966 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
967 _,
968 _,
969 ) | (
970 Some(InitialRebootCondition::WithTpm),
971 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
972 _,
973 Some(_),
974 )
975 )
976 }
977
978 fn start_watchdog_tasks(
979 resources: &PetriVmResources,
980 runtime: &mut T::VmRuntime,
981 enable_screenshots: bool,
982 ) -> anyhow::Result<Vec<Task<()>>> {
983 let mut tasks = Vec::new();
984
985 {
986 const TIMEOUT_DURATION_MINUTES: u64 = 10;
987 const TIMER_DURATION: Duration = Duration::from_secs(TIMEOUT_DURATION_MINUTES * 60);
988 let log_source = resources.log_source.clone();
989 let inspect_task =
990 |name,
991 driver: &DefaultDriver,
992 inspect: std::pin::Pin<Box<dyn Future<Output = _> + Send>>| {
993 driver.spawn(format!("petri-watchdog-inspect-{name}"), async move {
994 if CancelContext::new()
995 .with_timeout(Duration::from_secs(10))
996 .until_cancelled(save_inspect(name, inspect, &log_source))
997 .await
998 .is_err()
999 {
1000 tracing::warn!(name, "Failed to collect inspect data within timeout");
1001 }
1002 })
1003 };
1004
1005 let driver = resources.driver.clone();
1006 let vmm_inspector = runtime.inspector();
1007 let openhcl_diag_handler = runtime.openhcl_diag();
1008 tasks.push(resources.driver.spawn("timer-watchdog", async move {
1009 PolledTimer::new(&driver).sleep(TIMER_DURATION).await;
1010 tracing::warn!("Test timeout reached after {TIMEOUT_DURATION_MINUTES} minutes, collecting diagnostics.");
1011 let mut timeout_tasks = Vec::new();
1012 if let Some(inspector) = vmm_inspector {
1013 timeout_tasks.push(inspect_task.clone()("vmm", &driver, Box::pin(async move { inspector.inspect_all().await })) );
1014 }
1015 if let Some(openhcl_diag_handler) = openhcl_diag_handler {
1016 timeout_tasks.push(inspect_task("openhcl", &driver, Box::pin(async move { openhcl_diag_handler.inspect("", None, None).await })));
1017 }
1018 futures::future::join_all(timeout_tasks).await;
1019 tracing::error!("Test time out diagnostics collection complete, aborting.");
1020 panic!("Test timed out");
1021 }));
1022 }
1023
1024 if enable_screenshots {
1025 if let Some(mut framebuffer_access) = runtime.take_framebuffer_access() {
1026 let mut timer = PolledTimer::new(&resources.driver);
1027 let log_source = resources.log_source.clone();
1028
1029 tasks.push(
1030 resources
1031 .driver
1032 .spawn("petri-watchdog-screenshot", async move {
1033 let mut image = Vec::new();
1034 let mut last_image = Vec::new();
1035 loop {
1036 timer.sleep(Duration::from_secs(2)).await;
1037 tracing::trace!("Taking screenshot.");
1038
1039 let VmScreenshotMeta {
1040 color,
1041 width,
1042 height,
1043 } = match framebuffer_access.screenshot(&mut image).await {
1044 Ok(Some(meta)) => meta,
1045 Ok(None) => {
1046 tracing::debug!("VM off, skipping screenshot.");
1047 continue;
1048 }
1049 Err(e) => {
1050 tracing::error!(?e, "Failed to take screenshot");
1051 continue;
1052 }
1053 };
1054
1055 if image == last_image {
1056 tracing::debug!(
1057 "No change in framebuffer, skipping screenshot."
1058 );
1059 continue;
1060 }
1061
1062 let r = log_source.create_attachment("screenshot.png").and_then(
1063 |mut f| {
1064 image::write_buffer_with_format(
1065 &mut f,
1066 &image,
1067 width.into(),
1068 height.into(),
1069 color,
1070 image::ImageFormat::Png,
1071 )
1072 .map_err(Into::into)
1073 },
1074 );
1075
1076 if let Err(e) = r {
1077 tracing::error!(?e, "Failed to save screenshot");
1078 } else {
1079 tracing::info!("Screenshot saved.");
1080 }
1081
1082 std::mem::swap(&mut image, &mut last_image);
1083 }
1084 }),
1085 );
1086 }
1087 }
1088
1089 Ok(tasks)
1090 }
1091
1092 pub fn with_expect_boot_failure(mut self) -> Self {
1095 self.expected_boot_event = Some(FirmwareEvent::BootFailed);
1096 self
1097 }
1098
1099 pub fn with_expect_no_boot_event(mut self) -> Self {
1102 self.expected_boot_event = None;
1103 self
1104 }
1105
1106 pub fn with_expect_reset(mut self) -> Self {
1110 self.override_expect_reset = true;
1111 self
1112 }
1113
1114 pub fn with_secure_boot(mut self) -> Self {
1116 self.config
1117 .firmware
1118 .uefi_config_mut()
1119 .expect("Secure boot is only supported for UEFI firmware.")
1120 .secure_boot_enabled = true;
1121
1122 match self.os_flavor() {
1123 OsFlavor::Windows => self.with_windows_secure_boot_template(),
1124 OsFlavor::Linux => self.with_uefi_ca_secure_boot_template(),
1125 _ => panic!(
1126 "Secure boot unsupported for OS flavor {:?}",
1127 self.os_flavor()
1128 ),
1129 }
1130 }
1131
1132 pub fn with_windows_secure_boot_template(mut self) -> Self {
1134 self.config
1135 .firmware
1136 .uefi_config_mut()
1137 .expect("Secure boot is only supported for UEFI firmware.")
1138 .secure_boot_template = Some(SecureBootTemplate::MicrosoftWindows);
1139 self
1140 }
1141
1142 pub fn with_uefi_ca_secure_boot_template(mut self) -> Self {
1144 self.config
1145 .firmware
1146 .uefi_config_mut()
1147 .expect("Secure boot is only supported for UEFI firmware.")
1148 .secure_boot_template = Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority);
1149 self
1150 }
1151
1152 pub fn with_processor_topology(mut self, topology: ProcessorTopology) -> Self {
1154 self.config.proc_topology = topology;
1155 self
1156 }
1157
1158 pub fn with_memory(mut self, memory: MemoryConfig) -> Self {
1160 self.config.memory = memory;
1161 self
1162 }
1163
1164 pub fn with_vtl2_base_address_type(mut self, address_type: Vtl2BaseAddressType) -> Self {
1169 self.config
1170 .firmware
1171 .openhcl_config_mut()
1172 .expect("OpenHCL firmware is required to set custom VTL2 address type.")
1173 .vtl2_base_address_type = Some(address_type);
1174 self
1175 }
1176
1177 pub fn with_custom_openhcl(mut self, artifact: ResolvedArtifact<impl IsOpenhclIgvm>) -> Self {
1179 match &mut self.config.firmware {
1180 Firmware::OpenhclLinuxDirect { igvm_path, .. }
1181 | Firmware::OpenhclPcat { igvm_path, .. }
1182 | Firmware::OpenhclUefi { igvm_path, .. } => {
1183 *igvm_path = artifact.erase();
1184 }
1185 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } | Firmware::Pcat { .. } => {
1186 panic!("Custom OpenHCL is only supported for OpenHCL firmware.")
1187 }
1188 }
1189 self
1190 }
1191
1192 pub fn with_openhcl_command_line(mut self, additional_command_line: &str) -> Self {
1194 append_cmdline(
1195 &mut self
1196 .config
1197 .firmware
1198 .openhcl_config_mut()
1199 .expect("OpenHCL command line is only supported for OpenHCL firmware.")
1200 .custom_command_line,
1201 additional_command_line,
1202 );
1203 self
1204 }
1205
1206 pub fn with_confidential_filtering(self) -> Self {
1208 if !self.config.firmware.is_openhcl() {
1209 panic!("Confidential filtering is only supported for OpenHCL");
1210 }
1211 self.with_openhcl_command_line(&format!(
1212 "{}=1 {}=0",
1213 underhill_confidentiality::OPENHCL_CONFIDENTIAL_ENV_VAR_NAME,
1214 underhill_confidentiality::OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME
1215 ))
1216 }
1217
1218 pub fn with_openhcl_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1220 self.config
1221 .firmware
1222 .openhcl_config_mut()
1223 .expect("OpenHCL firmware is required to set custom OpenHCL log levels.")
1224 .log_levels = levels;
1225 self
1226 }
1227
1228 pub fn with_host_log_levels(mut self, levels: OpenvmmLogConfig) -> Self {
1232 if let OpenvmmLogConfig::Custom(ref custom_levels) = levels {
1233 for key in custom_levels.keys() {
1234 if !["OPENVMM_LOG", "OPENVMM_SHOW_SPANS"].contains(&key.as_str()) {
1235 panic!("Unsupported OpenVMM log level key: {}", key);
1236 }
1237 }
1238 }
1239
1240 self.config.host_log_levels = Some(levels.clone());
1241 self
1242 }
1243
1244 pub fn with_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
1246 self.agent_image
1247 .as_mut()
1248 .expect("no guest pipette")
1249 .add_file(name, artifact);
1250 self
1251 }
1252
1253 pub fn with_openhcl_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
1255 self.openhcl_agent_image
1256 .as_mut()
1257 .expect("no openhcl pipette")
1258 .add_file(name, artifact);
1259 self
1260 }
1261
1262 pub fn with_uefi_frontpage(mut self, enable: bool) -> Self {
1264 self.config
1265 .firmware
1266 .uefi_config_mut()
1267 .expect("UEFI frontpage is only supported for UEFI firmware.")
1268 .disable_frontpage = !enable;
1269 self
1270 }
1271
1272 pub fn with_default_boot_always_attempt(mut self, enable: bool) -> Self {
1274 self.config
1275 .firmware
1276 .uefi_config_mut()
1277 .expect("Default boot always attempt is only supported for UEFI firmware.")
1278 .default_boot_always_attempt = enable;
1279 self
1280 }
1281
1282 pub fn with_vmbus_redirect(mut self, enable: bool) -> Self {
1284 self.config
1285 .firmware
1286 .openhcl_config_mut()
1287 .expect("VMBus redirection is only supported for OpenHCL firmware.")
1288 .vmbus_redirect = enable;
1289 self
1290 }
1291
1292 pub fn with_guest_state_lifetime(
1294 mut self,
1295 guest_state_lifetime: PetriGuestStateLifetime,
1296 ) -> Self {
1297 let disk = match self.config.vmgs {
1298 PetriVmgsResource::Disk(disk)
1299 | PetriVmgsResource::ReprovisionOnFailure(disk)
1300 | PetriVmgsResource::Reprovision(disk) => disk,
1301 PetriVmgsResource::Ephemeral => PetriVmgsDisk::default(),
1302 };
1303 self.config.vmgs = match guest_state_lifetime {
1304 PetriGuestStateLifetime::Disk => PetriVmgsResource::Disk(disk),
1305 PetriGuestStateLifetime::ReprovisionOnFailure => {
1306 PetriVmgsResource::ReprovisionOnFailure(disk)
1307 }
1308 PetriGuestStateLifetime::Reprovision => PetriVmgsResource::Reprovision(disk),
1309 PetriGuestStateLifetime::Ephemeral => {
1310 if !matches!(disk.disk, Disk::Memory(_)) {
1311 panic!("attempted to use ephemeral guest state after specifying backing vmgs")
1312 }
1313 PetriVmgsResource::Ephemeral
1314 }
1315 };
1316 self
1317 }
1318
1319 pub fn with_guest_state_encryption(mut self, policy: GuestStateEncryptionPolicy) -> Self {
1321 match &mut self.config.vmgs {
1322 PetriVmgsResource::Disk(vmgs)
1323 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1324 | PetriVmgsResource::Reprovision(vmgs) => {
1325 vmgs.encryption_policy = policy;
1326 }
1327 PetriVmgsResource::Ephemeral => {
1328 panic!("attempted to encrypt ephemeral guest state")
1329 }
1330 }
1331 self
1332 }
1333
1334 pub fn with_initial_vmgs(self, disk: ResolvedArtifact<impl IsTestVmgs>) -> Self {
1336 self.with_backing_vmgs(Disk::Differencing(disk.into()))
1337 }
1338
1339 pub fn with_persistent_vmgs(self, disk: impl AsRef<Path>) -> Self {
1341 self.with_backing_vmgs(Disk::Persistent(disk.as_ref().to_path_buf()))
1342 }
1343
1344 fn with_backing_vmgs(mut self, disk: Disk) -> Self {
1345 match &mut self.config.vmgs {
1346 PetriVmgsResource::Disk(vmgs)
1347 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1348 | PetriVmgsResource::Reprovision(vmgs) => {
1349 if !matches!(vmgs.disk, Disk::Memory(_)) {
1350 panic!("already specified a backing vmgs file");
1351 }
1352 vmgs.disk = disk;
1353 }
1354 PetriVmgsResource::Ephemeral => {
1355 panic!("attempted to specify a backing vmgs with ephemeral guest state")
1356 }
1357 }
1358 self
1359 }
1360
1361 pub fn with_boot_device_type(mut self, boot: BootDeviceType) -> Self {
1365 self.boot_device_type = boot;
1366 self
1367 }
1368
1369 pub fn with_tpm(mut self, enable: bool) -> Self {
1371 if enable {
1372 self.config.tpm.get_or_insert_default();
1373 } else {
1374 self.config.tpm = None;
1375 }
1376 self
1377 }
1378
1379 pub fn with_tpm_state_persistence(mut self, tpm_state_persistence: bool) -> Self {
1381 self.config
1382 .tpm
1383 .as_mut()
1384 .expect("TPM persistence requires a TPM")
1385 .no_persistent_secrets = !tpm_state_persistence;
1386 self
1387 }
1388
1389 pub fn with_custom_vtl2_settings(
1393 mut self,
1394 f: impl FnOnce(&mut Vtl2Settings) + 'static + Send + Sync,
1395 ) -> Self {
1396 f(self
1397 .config
1398 .firmware
1399 .vtl2_settings()
1400 .expect("Custom VTL 2 settings are only supported with OpenHCL"));
1401 self
1402 }
1403
1404 pub fn add_vtl2_storage_controller(self, controller: StorageController) -> Self {
1406 self.with_custom_vtl2_settings(move |v| {
1407 v.dynamic
1408 .as_mut()
1409 .unwrap()
1410 .storage_controllers
1411 .push(controller)
1412 })
1413 }
1414
1415 pub fn add_vmbus_storage_controller(
1417 mut self,
1418 id: &Guid,
1419 target_vtl: Vtl,
1420 controller_type: VmbusStorageType,
1421 ) -> Self {
1422 if self
1423 .config
1424 .vmbus_storage_controllers
1425 .insert(
1426 *id,
1427 VmbusStorageController::new(target_vtl, controller_type),
1428 )
1429 .is_some()
1430 {
1431 panic!("storage controller {id} already existed");
1432 }
1433 self
1434 }
1435
1436 pub fn add_vmbus_drive(
1438 mut self,
1439 drive: Drive,
1440 controller_id: &Guid,
1441 controller_location: Option<u32>,
1442 ) -> Self {
1443 let controller = self
1444 .config
1445 .vmbus_storage_controllers
1446 .get_mut(controller_id)
1447 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
1448
1449 _ = controller.set_drive(controller_location, drive, false);
1450
1451 self
1452 }
1453
1454 pub fn add_ide_drive(
1456 mut self,
1457 drive: Drive,
1458 controller_number: u32,
1459 controller_location: u8,
1460 ) -> Self {
1461 self.config
1462 .firmware
1463 .ide_controllers_mut()
1464 .expect("Host IDE requires PCAT with no HCL")[controller_number as usize]
1465 [controller_location as usize] = Some(drive);
1466
1467 self
1468 }
1469
1470 pub fn os_flavor(&self) -> OsFlavor {
1472 self.config.firmware.os_flavor()
1473 }
1474
1475 pub fn is_openhcl(&self) -> bool {
1477 self.config.firmware.is_openhcl()
1478 }
1479
1480 pub fn isolation(&self) -> Option<IsolationType> {
1482 self.config.firmware.isolation()
1483 }
1484
1485 pub fn arch(&self) -> MachineArch {
1487 self.config.arch
1488 }
1489
1490 pub fn log_source(&self) -> &PetriLogSource {
1492 &self.resources.log_source
1493 }
1494
1495 pub fn default_servicing_flags(&self) -> OpenHclServicingFlags {
1497 T::default_servicing_flags()
1498 }
1499
1500 pub fn modify_backend(
1502 mut self,
1503 f: impl FnOnce(T::VmmConfig) -> T::VmmConfig + 'static + Send,
1504 ) -> Self {
1505 if self.modify_vmm_config.is_some() {
1506 panic!("only one modify_backend allowed");
1507 }
1508 self.modify_vmm_config = Some(ModifyFn(Box::new(f)));
1509 self
1510 }
1511}
1512
1513impl<T: PetriVmmBackend> PetriVm<T> {
1514 pub async fn teardown(self) -> anyhow::Result<()> {
1516 tracing::info!("Tearing down VM...");
1517 self.runtime.teardown().await
1518 }
1519
1520 pub async fn wait_for_halt(&mut self) -> anyhow::Result<PetriHaltReason> {
1522 tracing::info!("Waiting for VM to halt...");
1523 let halt_reason = self.runtime.wait_for_halt(false).await?;
1524 tracing::info!("VM halted: {halt_reason:?}. Cancelling watchdogs...");
1525 futures::future::join_all(self.watchdog_tasks.drain(..).map(|t| t.cancel())).await;
1526 Ok(halt_reason)
1527 }
1528
1529 pub async fn wait_for_clean_shutdown(&mut self) -> anyhow::Result<()> {
1531 let halt_reason = self.wait_for_halt().await?;
1532 if halt_reason != PetriHaltReason::PowerOff {
1533 anyhow::bail!("Expected PowerOff, got {halt_reason:?}");
1534 }
1535 tracing::info!("VM was cleanly powered off and torn down.");
1536 Ok(())
1537 }
1538
1539 pub async fn wait_for_teardown(mut self) -> anyhow::Result<PetriHaltReason> {
1542 let halt_reason = self.wait_for_halt().await?;
1543 self.teardown().await?;
1544 Ok(halt_reason)
1545 }
1546
1547 pub async fn wait_for_clean_teardown(mut self) -> anyhow::Result<()> {
1549 self.wait_for_clean_shutdown().await?;
1550 self.teardown().await
1551 }
1552
1553 pub async fn wait_for_reset_no_agent(&mut self) -> anyhow::Result<()> {
1555 self.wait_for_reset_core().await?;
1556 self.wait_for_expected_boot_event().await?;
1557 Ok(())
1558 }
1559
1560 pub async fn wait_for_reset(&mut self) -> anyhow::Result<PipetteClient> {
1562 self.wait_for_reset_no_agent().await?;
1563 self.wait_for_agent().await
1564 }
1565
1566 async fn wait_for_reset_core(&mut self) -> anyhow::Result<()> {
1567 tracing::info!("Waiting for VM to reset...");
1568 let halt_reason = self.runtime.wait_for_halt(true).await?;
1569 if halt_reason != PetriHaltReason::Reset {
1570 anyhow::bail!("Expected reset, got {halt_reason:?}");
1571 }
1572 tracing::info!("VM reset.");
1573 Ok(())
1574 }
1575
1576 pub async fn inspect_openhcl(
1587 &self,
1588 path: impl Into<String>,
1589 depth: Option<usize>,
1590 timeout: Option<Duration>,
1591 ) -> anyhow::Result<inspect::Node> {
1592 self.openhcl_diag()?
1593 .inspect(path.into().as_str(), depth, timeout)
1594 .await
1595 }
1596
1597 pub async fn inspect_update_openhcl(
1607 &self,
1608 path: impl Into<String>,
1609 value: impl Into<String>,
1610 ) -> anyhow::Result<inspect::Value> {
1611 self.openhcl_diag()?
1612 .inspect_update(path.into(), value.into())
1613 .await
1614 }
1615
1616 pub async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()> {
1618 self.inspect_openhcl("", None, None).await.map(|_| ())
1619 }
1620
1621 pub async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()> {
1627 self.openhcl_diag()?.wait_for_vtl2().await
1628 }
1629
1630 pub async fn kmsg(&self) -> anyhow::Result<diag_client::kmsg_stream::KmsgStream> {
1632 self.openhcl_diag()?.kmsg().await
1633 }
1634
1635 pub async fn openhcl_core_dump(&self, name: &str, path: &Path) -> anyhow::Result<()> {
1638 self.openhcl_diag()?.core_dump(name, path).await
1639 }
1640
1641 pub async fn openhcl_crash(&self, name: &str) -> anyhow::Result<()> {
1643 self.openhcl_diag()?.crash(name).await
1644 }
1645
1646 async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient> {
1649 if !self.uses_pipette_as_init {
1659 self.runtime.wait_for_enlightened_shutdown_ready().await?;
1660 }
1661 self.runtime.wait_for_agent(false).await
1662 }
1663
1664 pub async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient> {
1668 self.launch_vtl2_pipette().await?;
1670 self.runtime.wait_for_agent(true).await
1671 }
1672
1673 async fn wait_for_expected_boot_event(&mut self) -> anyhow::Result<()> {
1680 if let Some(expected_event) = self.expected_boot_event {
1681 let event = self.wait_for_boot_event().await?;
1682
1683 anyhow::ensure!(
1684 event == expected_event,
1685 "Did not receive expected boot event"
1686 );
1687 } else {
1688 tracing::warn!("Boot event not emitted for configured firmware or manually ignored.");
1689 }
1690
1691 Ok(())
1692 }
1693
1694 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
1697 tracing::info!("Waiting for boot event...");
1698 let boot_event = loop {
1699 match CancelContext::new()
1700 .with_timeout(self.vmm_quirks.flaky_boot.unwrap_or(Duration::MAX))
1701 .until_cancelled(self.runtime.wait_for_boot_event())
1702 .await
1703 {
1704 Ok(res) => break res?,
1705 Err(_) => {
1706 tracing::error!("Did not get boot event in required time, resetting...");
1707 if let Some(inspector) = self.runtime.inspector() {
1708 save_inspect(
1709 "vmm",
1710 Box::pin(async move { inspector.inspect_all().await }),
1711 &self.resources.log_source,
1712 )
1713 .await;
1714 }
1715
1716 self.runtime.reset().await?;
1717 continue;
1718 }
1719 }
1720 };
1721 tracing::info!("Got boot event: {boot_event:?}");
1722 Ok(boot_event)
1723 }
1724
1725 pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
1728 tracing::info!("Waiting for enlightened shutdown to be ready");
1729 self.runtime.wait_for_enlightened_shutdown_ready().await?;
1730
1731 let mut wait_time = Duration::from_secs(10);
1737
1738 if let Some(duration) = self.guest_quirks.hyperv_shutdown_ic_sleep {
1740 wait_time += duration;
1741 }
1742
1743 tracing::info!(
1744 "Shutdown IC reported ready, waiting for an extra {}s",
1745 wait_time.as_secs()
1746 );
1747 PolledTimer::new(&self.resources.driver)
1748 .sleep(wait_time)
1749 .await;
1750
1751 tracing::info!("Sending enlightened shutdown command");
1752 self.runtime.send_enlightened_shutdown(kind).await
1753 }
1754
1755 pub async fn restart_openhcl(
1758 &mut self,
1759 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1760 flags: OpenHclServicingFlags,
1761 ) -> anyhow::Result<()> {
1762 self.runtime
1763 .restart_openhcl(&new_openhcl.erase(), flags)
1764 .await
1765 }
1766
1767 pub async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()> {
1770 self.runtime.update_command_line(command_line).await
1771 }
1772
1773 pub async fn add_pcie_device(
1775 &mut self,
1776 port_name: String,
1777 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
1778 ) -> anyhow::Result<()> {
1779 self.runtime.add_pcie_device(port_name, resource).await
1780 }
1781
1782 pub async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
1784 self.runtime.remove_pcie_device(port_name).await
1785 }
1786
1787 pub async fn save_openhcl(
1790 &mut self,
1791 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1792 flags: OpenHclServicingFlags,
1793 ) -> anyhow::Result<()> {
1794 self.runtime.save_openhcl(&new_openhcl.erase(), flags).await
1795 }
1796
1797 pub async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
1800 self.runtime.restore_openhcl().await
1801 }
1802
1803 pub fn arch(&self) -> MachineArch {
1805 self.arch
1806 }
1807
1808 pub fn backend(&mut self) -> &mut T::VmRuntime {
1810 &mut self.runtime
1811 }
1812
1813 async fn launch_vtl2_pipette(&self) -> anyhow::Result<()> {
1814 tracing::debug!("Launching VTL 2 pipette...");
1815
1816 let res = self
1818 .openhcl_diag()?
1819 .run_vtl2_command("sh", &["-c", "mkdir /cidata && mount LABEL=cidata /cidata"])
1820 .await?;
1821
1822 if !res.exit_status.success() {
1823 anyhow::bail!("Failed to mount VTL 2 pipette drive: {:?}", res);
1824 }
1825
1826 let res = self
1827 .openhcl_diag()?
1828 .run_detached_vtl2_command("sh", &["-c", "/cidata/pipette 2>&1 | logger &"])
1829 .await?;
1830
1831 if !res.success() {
1832 anyhow::bail!("Failed to spawn VTL 2 pipette: {:?}", res);
1833 }
1834
1835 Ok(())
1836 }
1837
1838 fn openhcl_diag(&self) -> anyhow::Result<&OpenHclDiagHandler> {
1839 if let Some(ohd) = self.openhcl_diag_handler.as_ref() {
1840 Ok(ohd)
1841 } else {
1842 anyhow::bail!("VM is not configured with OpenHCL")
1843 }
1844 }
1845
1846 pub async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
1848 self.runtime.get_guest_state_file().await
1849 }
1850
1851 pub async fn modify_vtl2_settings(
1853 &mut self,
1854 f: impl FnOnce(&mut Vtl2Settings),
1855 ) -> anyhow::Result<()> {
1856 if self.openhcl_diag_handler.is_none() {
1857 panic!("Custom VTL 2 settings are only supported with OpenHCL");
1858 }
1859 f(self
1860 .config
1861 .vtl2_settings
1862 .get_or_insert_with(default_vtl2_settings));
1863 self.runtime
1864 .set_vtl2_settings(self.config.vtl2_settings.as_ref().unwrap())
1865 .await
1866 }
1867
1868 pub fn get_vmbus_storage_controllers(&self) -> &HashMap<Guid, VmbusStorageController> {
1870 &self.config.vmbus_storage_controllers
1871 }
1872
1873 pub async fn set_vmbus_drive(
1875 &mut self,
1876 drive: Drive,
1877 controller_id: &Guid,
1878 controller_location: Option<u32>,
1879 ) -> anyhow::Result<()> {
1880 let controller = self
1881 .config
1882 .vmbus_storage_controllers
1883 .get_mut(controller_id)
1884 .unwrap_or_else(|| panic!("storage controller {controller_id} does not exist"));
1885
1886 let controller_location = controller.set_drive(controller_location, drive, true);
1887 let disk = controller.drives.get(&controller_location).unwrap();
1888
1889 self.runtime
1890 .set_vmbus_drive(disk, controller_id, controller_location)
1891 .await?;
1892
1893 Ok(())
1894 }
1895}
1896
1897#[async_trait]
1899pub trait PetriVmRuntime: Send + Sync + 'static {
1900 type VmInspector: PetriVmInspector;
1902 type VmFramebufferAccess: PetriVmFramebufferAccess;
1904
1905 async fn teardown(self) -> anyhow::Result<()>;
1907 async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReason>;
1910 async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>;
1912 fn openhcl_diag(&self) -> Option<OpenHclDiagHandler>;
1914 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent>;
1917 async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()>;
1920 async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
1922 async fn restart_openhcl(
1925 &mut self,
1926 new_openhcl: &ResolvedArtifact,
1927 flags: OpenHclServicingFlags,
1928 ) -> anyhow::Result<()>;
1929 async fn save_openhcl(
1933 &mut self,
1934 new_openhcl: &ResolvedArtifact,
1935 flags: OpenHclServicingFlags,
1936 ) -> anyhow::Result<()>;
1937 async fn restore_openhcl(&mut self) -> anyhow::Result<()>;
1940 async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()>;
1943 fn inspector(&self) -> Option<Self::VmInspector> {
1945 None
1946 }
1947 fn take_framebuffer_access(&mut self) -> Option<Self::VmFramebufferAccess> {
1950 None
1951 }
1952 async fn reset(&mut self) -> anyhow::Result<()>;
1954 async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
1956 Ok(None)
1957 }
1958 async fn set_vtl2_settings(&mut self, settings: &Vtl2Settings) -> anyhow::Result<()>;
1960 async fn set_vmbus_drive(
1962 &mut self,
1963 disk: &Drive,
1964 controller_id: &Guid,
1965 controller_location: u32,
1966 ) -> anyhow::Result<()>;
1967 async fn add_pcie_device(
1969 &mut self,
1970 port_name: String,
1971 resource: vm_resource::Resource<vm_resource::kind::PciDeviceHandleKind>,
1972 ) -> anyhow::Result<()> {
1973 let _ = (port_name, resource);
1974 anyhow::bail!("PCIe hotplug not supported by this backend")
1975 }
1976 async fn remove_pcie_device(&mut self, port_name: String) -> anyhow::Result<()> {
1978 let _ = port_name;
1979 anyhow::bail!("PCIe hotplug not supported by this backend")
1980 }
1981}
1982
1983#[async_trait]
1985pub trait PetriVmInspector: Send + Sync + 'static {
1986 async fn inspect_all(&self) -> anyhow::Result<inspect::Node>;
1988}
1989
1990pub struct NoPetriVmInspector;
1992#[async_trait]
1993impl PetriVmInspector for NoPetriVmInspector {
1994 async fn inspect_all(&self) -> anyhow::Result<inspect::Node> {
1995 unreachable!()
1996 }
1997}
1998
1999pub struct VmScreenshotMeta {
2001 pub color: image::ExtendedColorType,
2003 pub width: u16,
2005 pub height: u16,
2007}
2008
2009#[async_trait]
2011pub trait PetriVmFramebufferAccess: Send + 'static {
2012 async fn screenshot(&mut self, image: &mut Vec<u8>)
2015 -> anyhow::Result<Option<VmScreenshotMeta>>;
2016}
2017
2018pub struct NoPetriVmFramebufferAccess;
2020#[async_trait]
2021impl PetriVmFramebufferAccess for NoPetriVmFramebufferAccess {
2022 async fn screenshot(
2023 &mut self,
2024 _image: &mut Vec<u8>,
2025 ) -> anyhow::Result<Option<VmScreenshotMeta>> {
2026 unreachable!()
2027 }
2028}
2029
2030#[derive(Debug)]
2032pub struct ProcessorTopology {
2033 pub vp_count: u32,
2035 pub enable_smt: Option<bool>,
2037 pub vps_per_socket: Option<u32>,
2039 pub apic_mode: Option<ApicMode>,
2041}
2042
2043impl Default for ProcessorTopology {
2044 fn default() -> Self {
2045 Self {
2046 vp_count: 2,
2047 enable_smt: None,
2048 vps_per_socket: None,
2049 apic_mode: None,
2050 }
2051 }
2052}
2053
2054#[derive(Debug, Clone, Copy)]
2056pub enum ApicMode {
2057 Xapic,
2059 X2apicSupported,
2061 X2apicEnabled,
2063}
2064
2065#[derive(Debug)]
2067pub enum MmioConfig {
2068 Platform,
2070 Custom(Vec<MemoryRange>),
2073}
2074
2075#[derive(Debug)]
2077pub struct MemoryConfig {
2078 pub startup_bytes: u64,
2081 pub dynamic_memory_range: Option<(u64, u64)>,
2085 pub mmio_gaps: MmioConfig,
2087}
2088
2089impl Default for MemoryConfig {
2090 fn default() -> Self {
2091 Self {
2092 startup_bytes: 4 * 1024 * 1024 * 1024, dynamic_memory_range: None,
2094 mmio_gaps: MmioConfig::Platform,
2095 }
2096 }
2097}
2098
2099#[derive(Debug)]
2101pub struct UefiConfig {
2102 pub secure_boot_enabled: bool,
2104 pub secure_boot_template: Option<SecureBootTemplate>,
2106 pub disable_frontpage: bool,
2108 pub default_boot_always_attempt: bool,
2110 pub enable_vpci_boot: bool,
2112}
2113
2114impl Default for UefiConfig {
2115 fn default() -> Self {
2116 Self {
2117 secure_boot_enabled: false,
2118 secure_boot_template: None,
2119 disable_frontpage: true,
2120 default_boot_always_attempt: false,
2121 enable_vpci_boot: false,
2122 }
2123 }
2124}
2125
2126#[derive(Debug, Clone)]
2128pub enum OpenvmmLogConfig {
2129 TestDefault,
2133 BuiltInDefault,
2136 Custom(BTreeMap<String, String>),
2146}
2147
2148#[derive(Debug)]
2150pub struct OpenHclConfig {
2151 pub vmbus_redirect: bool,
2153 pub custom_command_line: Option<String>,
2157 pub log_levels: OpenvmmLogConfig,
2161 pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
2164 pub vtl2_settings: Option<Vtl2Settings>,
2166}
2167
2168impl OpenHclConfig {
2169 pub fn command_line(&self) -> String {
2172 let mut cmdline = self.custom_command_line.clone();
2173
2174 append_cmdline(&mut cmdline, "OPENHCL_MANA_KEEP_ALIVE=host,privatepool");
2176
2177 match &self.log_levels {
2178 OpenvmmLogConfig::TestDefault => {
2179 let default_log_levels = {
2180 let openhcl_tracing = if let Ok(x) =
2182 std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG"))
2183 {
2184 format!("OPENVMM_LOG={x}")
2185 } else {
2186 "OPENVMM_LOG=debug".to_owned()
2187 };
2188 let openhcl_show_spans = if let Ok(x) = std::env::var("OPENVMM_SHOW_SPANS") {
2189 format!("OPENVMM_SHOW_SPANS={x}")
2190 } else {
2191 "OPENVMM_SHOW_SPANS=true".to_owned()
2192 };
2193 format!("{openhcl_tracing} {openhcl_show_spans}")
2194 };
2195 append_cmdline(&mut cmdline, &default_log_levels);
2196 }
2197 OpenvmmLogConfig::BuiltInDefault => {
2198 }
2200 OpenvmmLogConfig::Custom(levels) => {
2201 levels.iter().for_each(|(key, value)| {
2202 append_cmdline(&mut cmdline, format!("{key}={value}"));
2203 });
2204 }
2205 }
2206
2207 cmdline.unwrap_or_default()
2208 }
2209}
2210
2211impl Default for OpenHclConfig {
2212 fn default() -> Self {
2213 Self {
2214 vmbus_redirect: false,
2215 custom_command_line: None,
2216 log_levels: OpenvmmLogConfig::TestDefault,
2217 vtl2_base_address_type: None,
2218 vtl2_settings: None,
2219 }
2220 }
2221}
2222
2223#[derive(Debug)]
2225pub struct TpmConfig {
2226 pub no_persistent_secrets: bool,
2228}
2229
2230impl Default for TpmConfig {
2231 fn default() -> Self {
2232 Self {
2233 no_persistent_secrets: true,
2234 }
2235 }
2236}
2237
2238#[derive(Debug)]
2242pub enum Firmware {
2243 LinuxDirect {
2245 kernel: ResolvedArtifact,
2247 initrd: ResolvedArtifact,
2249 },
2250 OpenhclLinuxDirect {
2252 igvm_path: ResolvedArtifact,
2254 openhcl_config: OpenHclConfig,
2256 },
2257 Pcat {
2259 guest: PcatGuest,
2261 bios_firmware: ResolvedOptionalArtifact,
2263 svga_firmware: ResolvedOptionalArtifact,
2265 ide_controllers: [[Option<Drive>; 2]; 2],
2267 },
2268 OpenhclPcat {
2270 guest: PcatGuest,
2272 igvm_path: ResolvedArtifact,
2274 bios_firmware: ResolvedOptionalArtifact,
2276 svga_firmware: ResolvedOptionalArtifact,
2278 openhcl_config: OpenHclConfig,
2280 },
2281 Uefi {
2283 guest: UefiGuest,
2285 uefi_firmware: ResolvedArtifact,
2287 uefi_config: UefiConfig,
2289 },
2290 OpenhclUefi {
2292 guest: UefiGuest,
2294 isolation: Option<IsolationType>,
2296 igvm_path: ResolvedArtifact,
2298 uefi_config: UefiConfig,
2300 openhcl_config: OpenHclConfig,
2302 },
2303}
2304
2305#[derive(Debug, Copy, Clone, PartialEq, Eq)]
2307pub enum BootDeviceType {
2308 None,
2310 Ide,
2312 IdeViaScsi,
2314 IdeViaNvme,
2316 Scsi,
2318 ScsiViaScsi,
2320 ScsiViaNvme,
2322 Nvme,
2324 NvmeViaScsi,
2326 NvmeViaNvme,
2328}
2329
2330impl BootDeviceType {
2331 fn requires_vtl2(&self) -> bool {
2332 match self {
2333 BootDeviceType::None
2334 | BootDeviceType::Ide
2335 | BootDeviceType::Scsi
2336 | BootDeviceType::Nvme => false,
2337 BootDeviceType::IdeViaScsi
2338 | BootDeviceType::IdeViaNvme
2339 | BootDeviceType::ScsiViaScsi
2340 | BootDeviceType::ScsiViaNvme
2341 | BootDeviceType::NvmeViaScsi
2342 | BootDeviceType::NvmeViaNvme => true,
2343 }
2344 }
2345
2346 fn requires_vpci_boot(&self) -> bool {
2347 matches!(
2348 self,
2349 BootDeviceType::Nvme | BootDeviceType::NvmeViaScsi | BootDeviceType::NvmeViaNvme
2350 )
2351 }
2352}
2353
2354impl Firmware {
2355 pub fn linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2357 use petri_artifacts_vmm_test::artifacts::loadable::*;
2358 match arch {
2359 MachineArch::X86_64 => Firmware::LinuxDirect {
2360 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_X64).erase(),
2361 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
2362 },
2363 MachineArch::Aarch64 => Firmware::LinuxDirect {
2364 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_AARCH64).erase(),
2365 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_AARCH64).erase(),
2366 },
2367 }
2368 }
2369
2370 pub fn openhcl_linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2372 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2373 match arch {
2374 MachineArch::X86_64 => Firmware::OpenhclLinuxDirect {
2375 igvm_path: resolver.require(LATEST_LINUX_DIRECT_TEST_X64).erase(),
2376 openhcl_config: Default::default(),
2377 },
2378 MachineArch::Aarch64 => todo!("Linux direct not yet supported on aarch64"),
2379 }
2380 }
2381
2382 pub fn pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2384 use petri_artifacts_vmm_test::artifacts::loadable::*;
2385 Firmware::Pcat {
2386 guest,
2387 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2388 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2389 ide_controllers: [[None, None], [None, None]],
2390 }
2391 }
2392
2393 pub fn openhcl_pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
2395 use petri_artifacts_vmm_test::artifacts::loadable::*;
2396 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2397 Firmware::OpenhclPcat {
2398 guest,
2399 igvm_path: resolver.require(LATEST_STANDARD_X64).erase(),
2400 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
2401 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
2402 openhcl_config: OpenHclConfig {
2403 vmbus_redirect: true,
2405 ..Default::default()
2406 },
2407 }
2408 }
2409
2410 pub fn uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch, guest: UefiGuest) -> Self {
2412 use petri_artifacts_vmm_test::artifacts::loadable::*;
2413 let uefi_firmware = match arch {
2414 MachineArch::X86_64 => resolver.require(UEFI_FIRMWARE_X64).erase(),
2415 MachineArch::Aarch64 => resolver.require(UEFI_FIRMWARE_AARCH64).erase(),
2416 };
2417 Firmware::Uefi {
2418 guest,
2419 uefi_firmware,
2420 uefi_config: Default::default(),
2421 }
2422 }
2423
2424 pub fn openhcl_uefi(
2426 resolver: &ArtifactResolver<'_>,
2427 arch: MachineArch,
2428 guest: UefiGuest,
2429 isolation: Option<IsolationType>,
2430 ) -> Self {
2431 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
2432 let igvm_path = match arch {
2433 MachineArch::X86_64 if isolation.is_some() => resolver.require(LATEST_CVM_X64).erase(),
2434 MachineArch::X86_64 => resolver.require(LATEST_STANDARD_X64).erase(),
2435 MachineArch::Aarch64 => resolver.require(LATEST_STANDARD_AARCH64).erase(),
2436 };
2437 Firmware::OpenhclUefi {
2438 guest,
2439 isolation,
2440 igvm_path,
2441 uefi_config: Default::default(),
2442 openhcl_config: Default::default(),
2443 }
2444 }
2445
2446 fn is_openhcl(&self) -> bool {
2447 match self {
2448 Firmware::OpenhclLinuxDirect { .. }
2449 | Firmware::OpenhclUefi { .. }
2450 | Firmware::OpenhclPcat { .. } => true,
2451 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => false,
2452 }
2453 }
2454
2455 fn isolation(&self) -> Option<IsolationType> {
2456 match self {
2457 Firmware::OpenhclUefi { isolation, .. } => *isolation,
2458 Firmware::LinuxDirect { .. }
2459 | Firmware::Pcat { .. }
2460 | Firmware::Uefi { .. }
2461 | Firmware::OpenhclLinuxDirect { .. }
2462 | Firmware::OpenhclPcat { .. } => None,
2463 }
2464 }
2465
2466 fn is_linux_direct(&self) -> bool {
2467 match self {
2468 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => true,
2469 Firmware::Pcat { .. }
2470 | Firmware::Uefi { .. }
2471 | Firmware::OpenhclUefi { .. }
2472 | Firmware::OpenhclPcat { .. } => false,
2473 }
2474 }
2475
2476 pub fn linux_direct_initrd(&self) -> Option<&Path> {
2478 match self {
2479 Firmware::LinuxDirect { initrd, .. } => Some(initrd.get()),
2480 _ => None,
2481 }
2482 }
2483
2484 fn is_pcat(&self) -> bool {
2485 match self {
2486 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => true,
2487 Firmware::Uefi { .. }
2488 | Firmware::OpenhclUefi { .. }
2489 | Firmware::LinuxDirect { .. }
2490 | Firmware::OpenhclLinuxDirect { .. } => false,
2491 }
2492 }
2493
2494 fn os_flavor(&self) -> OsFlavor {
2495 match self {
2496 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => OsFlavor::Linux,
2497 Firmware::Uefi {
2498 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2499 ..
2500 }
2501 | Firmware::OpenhclUefi {
2502 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
2503 ..
2504 } => OsFlavor::Uefi,
2505 Firmware::Pcat {
2506 guest: PcatGuest::Vhd(cfg),
2507 ..
2508 }
2509 | Firmware::OpenhclPcat {
2510 guest: PcatGuest::Vhd(cfg),
2511 ..
2512 }
2513 | Firmware::Uefi {
2514 guest: UefiGuest::Vhd(cfg),
2515 ..
2516 }
2517 | Firmware::OpenhclUefi {
2518 guest: UefiGuest::Vhd(cfg),
2519 ..
2520 } => cfg.os_flavor,
2521 Firmware::Pcat {
2522 guest: PcatGuest::Iso(cfg),
2523 ..
2524 }
2525 | Firmware::OpenhclPcat {
2526 guest: PcatGuest::Iso(cfg),
2527 ..
2528 } => cfg.os_flavor,
2529 }
2530 }
2531
2532 fn quirks(&self) -> GuestQuirks {
2533 match self {
2534 Firmware::Pcat {
2535 guest: PcatGuest::Vhd(cfg),
2536 ..
2537 }
2538 | Firmware::Uefi {
2539 guest: UefiGuest::Vhd(cfg),
2540 ..
2541 }
2542 | Firmware::OpenhclUefi {
2543 guest: UefiGuest::Vhd(cfg),
2544 ..
2545 } => cfg.quirks.clone(),
2546 Firmware::Pcat {
2547 guest: PcatGuest::Iso(cfg),
2548 ..
2549 } => cfg.quirks.clone(),
2550 _ => Default::default(),
2551 }
2552 }
2553
2554 fn expected_boot_event(&self) -> Option<FirmwareEvent> {
2555 match self {
2556 Firmware::LinuxDirect { .. }
2557 | Firmware::OpenhclLinuxDirect { .. }
2558 | Firmware::Uefi {
2559 guest: UefiGuest::GuestTestUefi(_),
2560 ..
2561 }
2562 | Firmware::OpenhclUefi {
2563 guest: UefiGuest::GuestTestUefi(_),
2564 ..
2565 } => None,
2566 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => {
2567 Some(FirmwareEvent::BootAttempt)
2569 }
2570 Firmware::Uefi {
2571 guest: UefiGuest::None,
2572 ..
2573 }
2574 | Firmware::OpenhclUefi {
2575 guest: UefiGuest::None,
2576 ..
2577 } => Some(FirmwareEvent::NoBootDevice),
2578 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => {
2579 Some(FirmwareEvent::BootSuccess)
2580 }
2581 }
2582 }
2583
2584 fn openhcl_config(&self) -> Option<&OpenHclConfig> {
2585 match self {
2586 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2587 | Firmware::OpenhclUefi { openhcl_config, .. }
2588 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
2589 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2590 }
2591 }
2592
2593 fn openhcl_config_mut(&mut self) -> Option<&mut OpenHclConfig> {
2594 match self {
2595 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2596 | Firmware::OpenhclUefi { openhcl_config, .. }
2597 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
2598 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
2599 }
2600 }
2601
2602 fn into_runtime_config(
2603 self,
2604 vmbus_storage_controllers: HashMap<Guid, VmbusStorageController>,
2605 ) -> PetriVmRuntimeConfig {
2606 match self {
2607 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
2608 | Firmware::OpenhclUefi { openhcl_config, .. }
2609 | Firmware::OpenhclPcat { openhcl_config, .. } => PetriVmRuntimeConfig {
2610 vtl2_settings: Some(
2611 openhcl_config
2612 .vtl2_settings
2613 .unwrap_or_else(default_vtl2_settings),
2614 ),
2615 ide_controllers: None,
2616 vmbus_storage_controllers,
2617 },
2618 Firmware::Pcat {
2619 ide_controllers, ..
2620 } => PetriVmRuntimeConfig {
2621 vtl2_settings: None,
2622 ide_controllers: Some(ide_controllers),
2623 vmbus_storage_controllers,
2624 },
2625 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } => PetriVmRuntimeConfig {
2626 vtl2_settings: None,
2627 ide_controllers: None,
2628 vmbus_storage_controllers,
2629 },
2630 }
2631 }
2632
2633 fn uefi_config(&self) -> Option<&UefiConfig> {
2634 match self {
2635 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
2636 Some(uefi_config)
2637 }
2638 Firmware::LinuxDirect { .. }
2639 | Firmware::OpenhclLinuxDirect { .. }
2640 | Firmware::Pcat { .. }
2641 | Firmware::OpenhclPcat { .. } => None,
2642 }
2643 }
2644
2645 fn uefi_config_mut(&mut self) -> Option<&mut UefiConfig> {
2646 match self {
2647 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
2648 Some(uefi_config)
2649 }
2650 Firmware::LinuxDirect { .. }
2651 | Firmware::OpenhclLinuxDirect { .. }
2652 | Firmware::Pcat { .. }
2653 | Firmware::OpenhclPcat { .. } => None,
2654 }
2655 }
2656
2657 fn boot_drive(&self) -> Option<Drive> {
2658 match self {
2659 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => None,
2660 Firmware::Pcat { guest, .. } | Firmware::OpenhclPcat { guest, .. } => {
2661 Some((guest.artifact().to_owned(), guest.is_dvd()))
2662 }
2663 Firmware::Uefi { guest, .. } | Firmware::OpenhclUefi { guest, .. } => {
2664 guest.artifact().map(|a| (a.to_owned(), false))
2665 }
2666 }
2667 .map(|(artifact, is_dvd)| {
2668 Drive::new(
2669 Some(Disk::Differencing(artifact.get().to_path_buf())),
2670 is_dvd,
2671 )
2672 })
2673 }
2674
2675 fn vtl2_settings(&mut self) -> Option<&mut Vtl2Settings> {
2676 self.openhcl_config_mut()
2677 .map(|c| c.vtl2_settings.get_or_insert_with(default_vtl2_settings))
2678 }
2679
2680 fn ide_controllers(&self) -> Option<&[[Option<Drive>; 2]; 2]> {
2681 match self {
2682 Firmware::Pcat {
2683 ide_controllers, ..
2684 } => Some(ide_controllers),
2685 _ => None,
2686 }
2687 }
2688
2689 fn ide_controllers_mut(&mut self) -> Option<&mut [[Option<Drive>; 2]; 2]> {
2690 match self {
2691 Firmware::Pcat {
2692 ide_controllers, ..
2693 } => Some(ide_controllers),
2694 _ => None,
2695 }
2696 }
2697}
2698
2699#[derive(Debug)]
2702pub enum PcatGuest {
2703 Vhd(BootImageConfig<boot_image_type::Vhd>),
2705 Iso(BootImageConfig<boot_image_type::Iso>),
2707}
2708
2709impl PcatGuest {
2710 fn artifact(&self) -> &ResolvedArtifact {
2711 match self {
2712 PcatGuest::Vhd(disk) => &disk.artifact,
2713 PcatGuest::Iso(disk) => &disk.artifact,
2714 }
2715 }
2716
2717 fn is_dvd(&self) -> bool {
2718 matches!(self, Self::Iso(_))
2719 }
2720}
2721
2722#[derive(Debug)]
2725pub enum UefiGuest {
2726 Vhd(BootImageConfig<boot_image_type::Vhd>),
2728 GuestTestUefi(ResolvedArtifact),
2730 None,
2732}
2733
2734impl UefiGuest {
2735 pub fn guest_test_uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
2737 use petri_artifacts_vmm_test::artifacts::test_vhd::*;
2738 let artifact = match arch {
2739 MachineArch::X86_64 => resolver.require(GUEST_TEST_UEFI_X64).erase(),
2740 MachineArch::Aarch64 => resolver.require(GUEST_TEST_UEFI_AARCH64).erase(),
2741 };
2742 UefiGuest::GuestTestUefi(artifact)
2743 }
2744
2745 fn artifact(&self) -> Option<&ResolvedArtifact> {
2746 match self {
2747 UefiGuest::Vhd(vhd) => Some(&vhd.artifact),
2748 UefiGuest::GuestTestUefi(p) => Some(p),
2749 UefiGuest::None => None,
2750 }
2751 }
2752}
2753
2754pub mod boot_image_type {
2756 mod private {
2757 pub trait Sealed {}
2758 impl Sealed for super::Vhd {}
2759 impl Sealed for super::Iso {}
2760 }
2761
2762 pub trait BootImageType: private::Sealed {}
2765
2766 #[derive(Debug)]
2768 pub enum Vhd {}
2769
2770 #[derive(Debug)]
2772 pub enum Iso {}
2773
2774 impl BootImageType for Vhd {}
2775 impl BootImageType for Iso {}
2776}
2777
2778#[derive(Debug)]
2780pub struct BootImageConfig<T: boot_image_type::BootImageType> {
2781 artifact: ResolvedArtifact,
2783 os_flavor: OsFlavor,
2785 quirks: GuestQuirks,
2789 _type: core::marker::PhantomData<T>,
2791}
2792
2793impl BootImageConfig<boot_image_type::Vhd> {
2794 pub fn from_vhd<A>(artifact: ResolvedArtifact<A>) -> Self
2796 where
2797 A: petri_artifacts_common::tags::IsTestVhd,
2798 {
2799 BootImageConfig {
2800 artifact: artifact.erase(),
2801 os_flavor: A::OS_FLAVOR,
2802 quirks: A::quirks(),
2803 _type: std::marker::PhantomData,
2804 }
2805 }
2806}
2807
2808impl BootImageConfig<boot_image_type::Iso> {
2809 pub fn from_iso<A>(artifact: ResolvedArtifact<A>) -> Self
2811 where
2812 A: petri_artifacts_common::tags::IsTestIso,
2813 {
2814 BootImageConfig {
2815 artifact: artifact.erase(),
2816 os_flavor: A::OS_FLAVOR,
2817 quirks: A::quirks(),
2818 _type: std::marker::PhantomData,
2819 }
2820 }
2821}
2822
2823#[derive(Debug, Clone, Copy)]
2825pub enum IsolationType {
2826 Vbs,
2828 Snp,
2830 Tdx,
2832}
2833
2834#[derive(Debug, Clone, Copy)]
2836pub struct OpenHclServicingFlags {
2837 pub enable_nvme_keepalive: bool,
2840 pub enable_mana_keepalive: bool,
2842 pub override_version_checks: bool,
2844 pub stop_timeout_hint_secs: Option<u16>,
2846}
2847
2848#[derive(Debug, Clone)]
2850pub enum Disk {
2851 Memory(u64),
2853 Differencing(PathBuf),
2855 Persistent(PathBuf),
2857 Temporary(Arc<TempPath>),
2859}
2860
2861#[derive(Debug, Clone)]
2863pub struct PetriVmgsDisk {
2864 pub disk: Disk,
2866 pub encryption_policy: GuestStateEncryptionPolicy,
2868}
2869
2870impl Default for PetriVmgsDisk {
2871 fn default() -> Self {
2872 PetriVmgsDisk {
2873 disk: Disk::Memory(vmgs_format::VMGS_DEFAULT_CAPACITY),
2874 encryption_policy: GuestStateEncryptionPolicy::None(false),
2876 }
2877 }
2878}
2879
2880#[derive(Debug, Clone)]
2882pub enum PetriVmgsResource {
2883 Disk(PetriVmgsDisk),
2885 ReprovisionOnFailure(PetriVmgsDisk),
2887 Reprovision(PetriVmgsDisk),
2889 Ephemeral,
2891}
2892
2893impl PetriVmgsResource {
2894 pub fn disk(&self) -> Option<&PetriVmgsDisk> {
2896 match self {
2897 PetriVmgsResource::Disk(vmgs)
2898 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
2899 | PetriVmgsResource::Reprovision(vmgs) => Some(vmgs),
2900 PetriVmgsResource::Ephemeral => None,
2901 }
2902 }
2903}
2904
2905#[derive(Debug, Clone, Copy)]
2907pub enum PetriGuestStateLifetime {
2908 Disk,
2911 ReprovisionOnFailure,
2913 Reprovision,
2915 Ephemeral,
2917}
2918
2919#[derive(Debug, Clone, Copy)]
2921pub enum SecureBootTemplate {
2922 MicrosoftWindows,
2924 MicrosoftUefiCertificateAuthority,
2926}
2927
2928#[derive(Default, Debug, Clone)]
2931pub struct VmmQuirks {
2932 pub flaky_boot: Option<Duration>,
2935}
2936
2937fn make_vm_safe_name(name: &str) -> String {
2943 const MAX_VM_NAME_LENGTH: usize = 100;
2944 const HASH_LENGTH: usize = 4;
2945 const MAX_PREFIX_LENGTH: usize = MAX_VM_NAME_LENGTH - HASH_LENGTH;
2946
2947 if name.len() <= MAX_VM_NAME_LENGTH {
2948 name.to_owned()
2949 } else {
2950 let mut hasher = DefaultHasher::new();
2952 name.hash(&mut hasher);
2953 let hash = hasher.finish();
2954
2955 let hash_suffix = format!("{:04x}", hash & 0xFFFF);
2957
2958 let truncated = &name[..MAX_PREFIX_LENGTH];
2960 tracing::debug!(
2961 "VM name too long ({}), truncating '{}' to '{}{}'",
2962 name.len(),
2963 name,
2964 truncated,
2965 hash_suffix
2966 );
2967
2968 format!("{}{}", truncated, hash_suffix)
2969 }
2970}
2971
2972#[derive(Debug, Clone, Copy, Eq, PartialEq)]
2974pub enum PetriHaltReason {
2975 PowerOff,
2977 Reset,
2979 Hibernate,
2981 TripleFault,
2983 Other,
2985}
2986
2987fn append_cmdline(cmd: &mut Option<String>, add_cmd: impl AsRef<str>) {
2988 if let Some(cmd) = cmd.as_mut() {
2989 cmd.push(' ');
2990 cmd.push_str(add_cmd.as_ref());
2991 } else {
2992 *cmd = Some(add_cmd.as_ref().to_string());
2993 }
2994}
2995
2996async fn save_inspect(
2997 name: &str,
2998 inspect: std::pin::Pin<Box<dyn Future<Output = anyhow::Result<inspect::Node>> + Send>>,
2999 log_source: &PetriLogSource,
3000) {
3001 tracing::info!("Collecting {name} inspect details.");
3002 let node = match inspect.await {
3003 Ok(n) => n,
3004 Err(e) => {
3005 tracing::error!(?e, "Failed to get {name}");
3006 return;
3007 }
3008 };
3009 if let Err(e) = log_source.write_attachment(
3010 &format!("timeout_inspect_{name}.log"),
3011 format!("{node:#}").as_bytes(),
3012 ) {
3013 tracing::error!(?e, "Failed to save {name} inspect log");
3014 return;
3015 }
3016 tracing::info!("{name} inspect task finished.");
3017}
3018
3019pub struct ModifyFn<T>(pub Box<dyn FnOnce(T) -> T + Send>);
3021
3022impl<T> Debug for ModifyFn<T> {
3023 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3024 write!(f, "_")
3025 }
3026}
3027
3028fn default_vtl2_settings() -> Vtl2Settings {
3030 Vtl2Settings {
3031 version: vtl2_settings_proto::vtl2_settings_base::Version::V1.into(),
3032 fixed: None,
3033 dynamic: Some(Default::default()),
3034 namespace_settings: Default::default(),
3035 }
3036}
3037
3038#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3040pub enum Vtl {
3041 Vtl0 = 0,
3043 Vtl1 = 1,
3045 Vtl2 = 2,
3047}
3048
3049#[derive(Debug, Copy, Clone, PartialEq, Eq)]
3051pub enum VmbusStorageType {
3052 Scsi,
3054 Nvme,
3056 VirtioBlk,
3058}
3059
3060#[derive(Debug, Clone)]
3062pub struct Drive {
3063 pub disk: Option<Disk>,
3065 pub is_dvd: bool,
3067}
3068
3069impl Drive {
3070 pub fn new(disk: Option<Disk>, is_dvd: bool) -> Self {
3072 Self { disk, is_dvd }
3073 }
3074}
3075
3076#[derive(Debug, Clone)]
3078pub struct VmbusStorageController {
3079 pub target_vtl: Vtl,
3081 pub controller_type: VmbusStorageType,
3083 pub drives: HashMap<u32, Drive>,
3085}
3086
3087impl VmbusStorageController {
3088 pub fn new(target_vtl: Vtl, controller_type: VmbusStorageType) -> Self {
3090 Self {
3091 target_vtl,
3092 controller_type,
3093 drives: HashMap::new(),
3094 }
3095 }
3096
3097 pub fn set_drive(
3099 &mut self,
3100 lun: Option<u32>,
3101 drive: Drive,
3102 allow_modify_existing: bool,
3103 ) -> u32 {
3104 let lun = lun.unwrap_or_else(|| {
3105 let mut lun = None;
3107 for x in 0..u8::MAX as u32 {
3108 if !self.drives.contains_key(&x) {
3109 lun = Some(x);
3110 break;
3111 }
3112 }
3113 lun.expect("all locations on this controller are in use")
3114 });
3115
3116 if self.drives.insert(lun, drive).is_some() && !allow_modify_existing {
3117 panic!("a disk with lun {lun} already existed on this controller");
3118 }
3119
3120 lun
3121 }
3122}
3123
3124#[cfg(test)]
3125mod tests {
3126 use super::make_vm_safe_name;
3127 use crate::Drive;
3128 use crate::VmbusStorageController;
3129 use crate::VmbusStorageType;
3130 use crate::Vtl;
3131
3132 #[test]
3133 fn test_short_names_unchanged() {
3134 let short_name = "short_test_name";
3135 assert_eq!(make_vm_safe_name(short_name), short_name);
3136 }
3137
3138 #[test]
3139 fn test_exactly_100_chars_unchanged() {
3140 let name_100 = "a".repeat(100);
3141 assert_eq!(make_vm_safe_name(&name_100), name_100);
3142 }
3143
3144 #[test]
3145 fn test_long_name_truncated() {
3146 let long_name = "multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_openhcl_servicing";
3147 let result = make_vm_safe_name(long_name);
3148
3149 assert_eq!(result.len(), 100);
3151
3152 assert!(result.starts_with("multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_ope"));
3154
3155 let suffix = &result[96..];
3157 assert_eq!(suffix.len(), 4);
3158 assert!(u16::from_str_radix(suffix, 16).is_ok());
3160 }
3161
3162 #[test]
3163 fn test_deterministic_results() {
3164 let long_name = "very_long_test_name_that_exceeds_the_100_character_limit_and_should_be_truncated_consistently_every_time";
3165 let result1 = make_vm_safe_name(long_name);
3166 let result2 = make_vm_safe_name(long_name);
3167
3168 assert_eq!(result1, result2);
3169 assert_eq!(result1.len(), 100);
3170 }
3171
3172 #[test]
3173 fn test_different_names_different_hashes() {
3174 let name1 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_1";
3175 let name2 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_2";
3176
3177 let result1 = make_vm_safe_name(name1);
3178 let result2 = make_vm_safe_name(name2);
3179
3180 assert_eq!(result1.len(), 100);
3182 assert_eq!(result2.len(), 100);
3183
3184 assert_ne!(result1, result2);
3186 assert_ne!(&result1[96..], &result2[96..]);
3187 }
3188
3189 #[test]
3190 fn test_vmbus_storage_controller() {
3191 let mut controller = VmbusStorageController::new(Vtl::Vtl0, VmbusStorageType::Scsi);
3192 assert_eq!(
3193 controller.set_drive(Some(1), Drive::new(None, false), false),
3194 1
3195 );
3196 assert!(controller.drives.contains_key(&1));
3197 assert_eq!(
3198 controller.set_drive(None, Drive::new(None, false), false),
3199 0
3200 );
3201 assert!(controller.drives.contains_key(&0));
3202 assert_eq!(
3203 controller.set_drive(None, Drive::new(None, false), false),
3204 2
3205 );
3206 assert!(controller.drives.contains_key(&2));
3207 assert_eq!(
3208 controller.set_drive(Some(0), Drive::new(None, false), true),
3209 0
3210 );
3211 assert!(controller.drives.contains_key(&0));
3212 }
3213}