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 async_trait::async_trait;
18use get_resources::ged::FirmwareEvent;
19use hvlite_defs::config::Vtl2BaseAddressType;
20use mesh::CancelContext;
21use pal_async::DefaultDriver;
22use pal_async::task::Spawn;
23use pal_async::task::Task;
24use pal_async::timer::PolledTimer;
25use petri_artifacts_common::tags::GuestQuirks;
26use petri_artifacts_common::tags::GuestQuirksInner;
27use petri_artifacts_common::tags::InitialRebootCondition;
28use petri_artifacts_common::tags::IsOpenhclIgvm;
29use petri_artifacts_common::tags::IsTestVmgs;
30use petri_artifacts_common::tags::MachineArch;
31use petri_artifacts_common::tags::OsFlavor;
32use petri_artifacts_core::ArtifactResolver;
33use petri_artifacts_core::ResolvedArtifact;
34use petri_artifacts_core::ResolvedOptionalArtifact;
35use pipette_client::PipetteClient;
36use std::collections::hash_map::DefaultHasher;
37use std::hash::Hash;
38use std::hash::Hasher;
39use std::path::Path;
40use std::path::PathBuf;
41use std::sync::Arc;
42use std::time::Duration;
43use tempfile::TempPath;
44use vmgs_resources::GuestStateEncryptionPolicy;
45
46pub struct PetriVmArtifacts<T: PetriVmmBackend> {
49 pub backend: T,
51 pub firmware: Firmware,
53 pub arch: MachineArch,
55 pub agent_image: Option<AgentImage>,
57 pub openhcl_agent_image: Option<AgentImage>,
59}
60
61impl<T: PetriVmmBackend> PetriVmArtifacts<T> {
62 pub fn new(
66 resolver: &ArtifactResolver<'_>,
67 firmware: Firmware,
68 arch: MachineArch,
69 with_vtl0_pipette: bool,
70 ) -> Option<Self> {
71 if !T::check_compat(&firmware, arch) {
72 return None;
73 }
74
75 Some(Self {
76 backend: T::new(resolver),
77 arch,
78 agent_image: Some(if with_vtl0_pipette {
79 AgentImage::new(firmware.os_flavor()).with_pipette(resolver, arch)
80 } else {
81 AgentImage::new(firmware.os_flavor())
82 }),
83 openhcl_agent_image: if firmware.is_openhcl() {
84 Some(AgentImage::new(OsFlavor::Linux).with_pipette(resolver, arch))
85 } else {
86 None
87 },
88 firmware,
89 })
90 }
91}
92
93pub struct PetriVmBuilder<T: PetriVmmBackend> {
95 backend: T,
97 config: PetriVmConfig,
99 modify_vmm_config: Option<Box<dyn FnOnce(T::VmmConfig) -> T::VmmConfig + Send>>,
101 resources: PetriVmResources,
103
104 guest_quirks: GuestQuirksInner,
106 vmm_quirks: VmmQuirks,
107
108 expected_boot_event: Option<FirmwareEvent>,
111 override_expect_reset: bool,
112}
113
114pub struct PetriVmConfig {
116 pub name: String,
118 pub arch: MachineArch,
120 pub firmware: Firmware,
122 pub memory: MemoryConfig,
124 pub proc_topology: ProcessorTopology,
126 pub agent_image: Option<AgentImage>,
128 pub openhcl_agent_image: Option<AgentImage>,
130 pub guest_crash_disk: Option<Arc<TempPath>>,
132 pub vmgs: PetriVmgsResource,
134 pub boot_device_type: BootDeviceType,
136 pub tpm_state_persistence: bool,
138}
139
140pub struct PetriVmResources {
142 driver: DefaultDriver,
143 log_source: PetriLogSource,
144}
145
146#[async_trait]
148pub trait PetriVmmBackend {
149 type VmmConfig;
151
152 type VmRuntime: PetriVmRuntime;
154
155 fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool;
158
159 fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks);
161
162 fn default_servicing_flags() -> OpenHclServicingFlags;
164
165 fn create_guest_dump_disk() -> anyhow::Result<
168 Option<(
169 Arc<TempPath>,
170 Box<dyn FnOnce() -> anyhow::Result<Box<dyn fatfs::ReadWriteSeek>>>,
171 )>,
172 >;
173
174 fn new(resolver: &ArtifactResolver<'_>) -> Self;
176
177 async fn run(
179 self,
180 config: PetriVmConfig,
181 modify_vmm_config: Option<impl FnOnce(Self::VmmConfig) -> Self::VmmConfig + Send>,
182 resources: &PetriVmResources,
183 ) -> anyhow::Result<Self::VmRuntime>;
184}
185
186pub(crate) const PETRI_VTL0_SCSI_BOOT_LUN: u8 = 0;
187pub(crate) const PETRI_VTL0_SCSI_PIPETTE_LUN: u8 = 1;
188pub(crate) const PETRI_VTL0_SCSI_CRASH_LUN: u8 = 2;
189
190pub struct PetriVm<T: PetriVmmBackend> {
192 resources: PetriVmResources,
193 runtime: T::VmRuntime,
194 watchdog_tasks: Vec<Task<()>>,
195 openhcl_diag_handler: Option<OpenHclDiagHandler>,
196
197 arch: MachineArch,
198 guest_quirks: GuestQuirksInner,
199 vmm_quirks: VmmQuirks,
200 expected_boot_event: Option<FirmwareEvent>,
201}
202
203impl<T: PetriVmmBackend> PetriVmBuilder<T> {
204 pub fn new(
206 params: PetriTestParams<'_>,
207 artifacts: PetriVmArtifacts<T>,
208 driver: &DefaultDriver,
209 ) -> anyhow::Result<Self> {
210 let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
211 let expected_boot_event = artifacts.firmware.expected_boot_event();
212 let boot_device_type = match artifacts.firmware {
213 Firmware::LinuxDirect { .. } => BootDeviceType::None,
214 Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
215 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => BootDeviceType::Ide,
216 Firmware::Uefi {
217 guest: UefiGuest::None,
218 ..
219 }
220 | Firmware::OpenhclUefi {
221 guest: UefiGuest::None,
222 ..
223 } => BootDeviceType::None,
224 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
225 };
226
227 let guest_crash_disk = if matches!(
228 artifacts.firmware.os_flavor(),
229 OsFlavor::Windows | OsFlavor::Linux
230 ) {
231 let (guest_crash_disk, guest_dump_disk_hook) = T::create_guest_dump_disk()?.unzip();
232 if let Some(guest_dump_disk_hook) = guest_dump_disk_hook {
233 let logger = params.logger.clone();
234 params
235 .post_test_hooks
236 .push(crate::test::PetriPostTestHook::new(
237 "extract guest crash dumps".into(),
238 move |test_passed| {
239 if test_passed {
240 return Ok(());
241 }
242 let mut disk = guest_dump_disk_hook()?;
243 let gpt = gptman::GPT::read_from(&mut disk, SECTOR_SIZE)?;
244 let partition = fscommon::StreamSlice::new(
245 &mut disk,
246 gpt[1].starting_lba * SECTOR_SIZE,
247 gpt[1].ending_lba * SECTOR_SIZE,
248 )?;
249 let fs = fatfs::FileSystem::new(partition, fatfs::FsOptions::new())?;
250 for entry in fs.root_dir().iter() {
251 let Ok(entry) = entry else {
252 tracing::warn!(
253 ?entry,
254 "failed to read entry in guest crash dump disk"
255 );
256 continue;
257 };
258 if !entry.is_file() {
259 tracing::warn!(
260 ?entry,
261 "skipping non-file entry in guest crash dump disk"
262 );
263 continue;
264 }
265 logger.write_attachment(&entry.file_name(), entry.to_file())?;
266 }
267 Ok(())
268 },
269 ));
270 }
271 guest_crash_disk
272 } else {
273 None
274 };
275
276 Ok(Self {
277 backend: artifacts.backend,
278 config: PetriVmConfig {
279 name: make_vm_safe_name(params.test_name),
280 arch: artifacts.arch,
281 firmware: artifacts.firmware,
282 boot_device_type,
283 memory: Default::default(),
284 proc_topology: Default::default(),
285 agent_image: artifacts.agent_image,
286 openhcl_agent_image: artifacts.openhcl_agent_image,
287 vmgs: PetriVmgsResource::Ephemeral,
288 tpm_state_persistence: true,
289 guest_crash_disk,
290 },
291 modify_vmm_config: None,
292 resources: PetriVmResources {
293 driver: driver.clone(),
294 log_source: params.logger.clone(),
295 },
296
297 guest_quirks,
298 vmm_quirks,
299 expected_boot_event,
300 override_expect_reset: false,
301 })
302 }
303}
304
305impl<T: PetriVmmBackend> PetriVmBuilder<T> {
306 pub async fn run_without_agent(self) -> anyhow::Result<PetriVm<T>> {
310 self.run_core().await
311 }
312
313 pub async fn run(self) -> anyhow::Result<(PetriVm<T>, PipetteClient)> {
316 assert!(self.config.agent_image.is_some());
317 assert!(self.config.agent_image.as_ref().unwrap().contains_pipette());
318
319 let mut vm = self.run_core().await?;
320 let client = vm.wait_for_agent().await?;
321 Ok((vm, client))
322 }
323
324 async fn run_core(self) -> anyhow::Result<PetriVm<T>> {
325 let arch = self.config.arch;
326 let expect_reset = self.expect_reset();
327
328 let mut runtime = self
329 .backend
330 .run(self.config, self.modify_vmm_config, &self.resources)
331 .await?;
332 let openhcl_diag_handler = runtime.openhcl_diag();
333 let watchdog_tasks = Self::start_watchdog_tasks(&self.resources, &mut runtime)?;
334
335 let mut vm = PetriVm {
336 resources: self.resources,
337 runtime,
338 watchdog_tasks,
339 openhcl_diag_handler,
340
341 arch,
342 guest_quirks: self.guest_quirks,
343 vmm_quirks: self.vmm_quirks,
344 expected_boot_event: self.expected_boot_event,
345 };
346
347 if expect_reset {
348 vm.wait_for_reset_core().await?;
349 }
350
351 vm.wait_for_expected_boot_event().await?;
352
353 Ok(vm)
354 }
355
356 fn expect_reset(&self) -> bool {
357 self.override_expect_reset
359 || matches!(
360 (
361 self.guest_quirks.initial_reboot,
362 self.expected_boot_event,
363 &self.config.firmware,
364 ),
365 (
366 Some(InitialRebootCondition::Always),
367 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
368 _,
369 ) | (
370 Some(InitialRebootCondition::WithOpenHclUefi),
371 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
372 Firmware::OpenhclUefi { .. },
373 )
374 )
375 }
376
377 fn start_watchdog_tasks(
378 resources: &PetriVmResources,
379 runtime: &mut T::VmRuntime,
380 ) -> anyhow::Result<Vec<Task<()>>> {
381 let mut tasks = Vec::new();
382
383 {
384 const TIMEOUT_DURATION_MINUTES: u64 = 10;
385 const TIMER_DURATION: Duration = Duration::from_secs(TIMEOUT_DURATION_MINUTES * 60);
386 let log_source = resources.log_source.clone();
387 let inspect_task =
388 |name,
389 driver: &DefaultDriver,
390 inspect: std::pin::Pin<Box<dyn Future<Output = _> + Send>>| {
391 driver.spawn(format!("petri-watchdog-inspect-{name}"), async move {
392 save_inspect(name, inspect, &log_source).await;
393 })
394 };
395
396 let driver = resources.driver.clone();
397 let vmm_inspector = runtime.inspector();
398 let openhcl_diag_handler = runtime.openhcl_diag();
399 tasks.push(resources.driver.spawn("timer-watchdog", async move {
400 PolledTimer::new(&driver).sleep(TIMER_DURATION).await;
401 tracing::warn!("Test timeout reached after {TIMEOUT_DURATION_MINUTES} minutes, collecting diagnostics.");
402 let mut timeout_tasks = Vec::new();
403 if let Some(inspector) = vmm_inspector {
404 timeout_tasks.push(inspect_task.clone()("vmm", &driver, Box::pin(async move { inspector.inspect_all().await })) );
405 }
406 if let Some(openhcl_diag_handler) = openhcl_diag_handler {
407 timeout_tasks.push(inspect_task("openhcl", &driver, Box::pin(async move { openhcl_diag_handler.inspect("", None, None).await })));
408 }
409 futures::future::join_all(timeout_tasks).await;
410 tracing::error!("Test time out diagnostics collection complete, aborting.");
411 panic!("Test timed out");
412 }));
413 }
414
415 if let Some(mut framebuffer_access) = runtime.take_framebuffer_access() {
416 let mut timer = PolledTimer::new(&resources.driver);
417 let log_source = resources.log_source.clone();
418
419 tasks.push(
420 resources
421 .driver
422 .spawn("petri-watchdog-screenshot", async move {
423 let mut image = Vec::new();
424 let mut last_image = Vec::new();
425 loop {
426 timer.sleep(Duration::from_secs(2)).await;
427 tracing::trace!("Taking screenshot.");
428
429 let VmScreenshotMeta {
430 color,
431 width,
432 height,
433 } = match framebuffer_access.screenshot(&mut image).await {
434 Ok(Some(meta)) => meta,
435 Ok(None) => {
436 tracing::debug!("VM off, skipping screenshot.");
437 continue;
438 }
439 Err(e) => {
440 tracing::error!(?e, "Failed to take screenshot");
441 continue;
442 }
443 };
444
445 if image == last_image {
446 tracing::debug!("No change in framebuffer, skipping screenshot.");
447 continue;
448 }
449
450 let r =
451 log_source
452 .create_attachment("screenshot.png")
453 .and_then(|mut f| {
454 image::write_buffer_with_format(
455 &mut f,
456 &image,
457 width.into(),
458 height.into(),
459 color,
460 image::ImageFormat::Png,
461 )
462 .map_err(Into::into)
463 });
464
465 if let Err(e) = r {
466 tracing::error!(?e, "Failed to save screenshot");
467 } else {
468 tracing::info!("Screenshot saved.");
469 }
470
471 std::mem::swap(&mut image, &mut last_image);
472 }
473 }),
474 );
475 }
476
477 Ok(tasks)
478 }
479
480 pub fn with_expect_boot_failure(mut self) -> Self {
483 self.expected_boot_event = Some(FirmwareEvent::BootFailed);
484 self
485 }
486
487 pub fn with_expect_no_boot_event(mut self) -> Self {
490 self.expected_boot_event = None;
491 self
492 }
493
494 pub fn with_expect_reset(mut self) -> Self {
498 self.override_expect_reset = true;
499 self
500 }
501
502 pub fn with_secure_boot(mut self) -> Self {
504 self.config
505 .firmware
506 .uefi_config_mut()
507 .expect("Secure boot is only supported for UEFI firmware.")
508 .secure_boot_enabled = true;
509
510 match self.os_flavor() {
511 OsFlavor::Windows => self.with_windows_secure_boot_template(),
512 OsFlavor::Linux => self.with_uefi_ca_secure_boot_template(),
513 _ => panic!(
514 "Secure boot unsupported for OS flavor {:?}",
515 self.os_flavor()
516 ),
517 }
518 }
519
520 pub fn with_windows_secure_boot_template(mut self) -> Self {
522 self.config
523 .firmware
524 .uefi_config_mut()
525 .expect("Secure boot is only supported for UEFI firmware.")
526 .secure_boot_template = Some(SecureBootTemplate::MicrosoftWindows);
527 self
528 }
529
530 pub fn with_uefi_ca_secure_boot_template(mut self) -> Self {
532 self.config
533 .firmware
534 .uefi_config_mut()
535 .expect("Secure boot is only supported for UEFI firmware.")
536 .secure_boot_template = Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority);
537 self
538 }
539
540 pub fn with_processor_topology(mut self, topology: ProcessorTopology) -> Self {
542 self.config.proc_topology = topology;
543 self
544 }
545
546 pub fn with_memory(mut self, memory: MemoryConfig) -> Self {
548 self.config.memory = memory;
549 self
550 }
551
552 pub fn with_vtl2_base_address_type(mut self, address_type: Vtl2BaseAddressType) -> Self {
557 self.config
558 .firmware
559 .openhcl_config_mut()
560 .expect("OpenHCL firmware is required to set custom VTL2 address type.")
561 .vtl2_base_address_type = Some(address_type);
562 self
563 }
564
565 pub fn with_custom_openhcl(mut self, artifact: ResolvedArtifact<impl IsOpenhclIgvm>) -> Self {
567 match &mut self.config.firmware {
568 Firmware::OpenhclLinuxDirect { igvm_path, .. }
569 | Firmware::OpenhclPcat { igvm_path, .. }
570 | Firmware::OpenhclUefi { igvm_path, .. } => {
571 *igvm_path = artifact.erase();
572 }
573 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } | Firmware::Pcat { .. } => {
574 panic!("Custom OpenHCL is only supported for OpenHCL firmware.")
575 }
576 }
577 self
578 }
579
580 pub fn with_openhcl_command_line(mut self, additional_command_line: &str) -> Self {
582 append_cmdline(
583 &mut self
584 .config
585 .firmware
586 .openhcl_config_mut()
587 .expect("OpenHCL command line is only supported for OpenHCL firmware.")
588 .command_line,
589 additional_command_line,
590 );
591 self
592 }
593
594 pub fn with_confidential_filtering(self) -> Self {
596 if !self.config.firmware.is_openhcl() {
597 panic!("Confidential filtering is only supported for OpenHCL");
598 }
599 self.with_openhcl_command_line(&format!(
600 "{}=1 {}=0",
601 underhill_confidentiality::OPENHCL_CONFIDENTIAL_ENV_VAR_NAME,
602 underhill_confidentiality::OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME
603 ))
604 }
605
606 pub fn with_openhcl_log_levels(mut self, levels: OpenHclLogConfig) -> Self {
608 self.config
609 .firmware
610 .openhcl_config_mut()
611 .expect("OpenHCL firmware is required to set custom OpenHCL log levels.")
612 .log_levels = levels;
613 self
614 }
615
616 pub fn with_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
618 self.config
619 .agent_image
620 .as_mut()
621 .expect("no guest pipette")
622 .add_file(name, artifact);
623 self
624 }
625
626 pub fn with_openhcl_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
628 self.config
629 .openhcl_agent_image
630 .as_mut()
631 .expect("no openhcl pipette")
632 .add_file(name, artifact);
633 self
634 }
635
636 pub fn with_uefi_frontpage(mut self, enable: bool) -> Self {
638 self.config
639 .firmware
640 .uefi_config_mut()
641 .expect("UEFI frontpage is only supported for UEFI firmware.")
642 .disable_frontpage = !enable;
643 self
644 }
645
646 pub fn with_default_boot_always_attempt(mut self, enable: bool) -> Self {
648 self.config
649 .firmware
650 .uefi_config_mut()
651 .expect("Default boot always attempt is only supported for UEFI firmware.")
652 .default_boot_always_attempt = enable;
653 self
654 }
655
656 pub fn with_vmbus_redirect(mut self, enable: bool) -> Self {
658 self.config
659 .firmware
660 .openhcl_config_mut()
661 .expect("VMBus redirection is only supported for OpenHCL firmware.")
662 .vmbus_redirect = enable;
663 self
664 }
665
666 pub fn with_guest_state_lifetime(
668 mut self,
669 guest_state_lifetime: PetriGuestStateLifetime,
670 ) -> Self {
671 let disk = match self.config.vmgs {
672 PetriVmgsResource::Disk(disk)
673 | PetriVmgsResource::ReprovisionOnFailure(disk)
674 | PetriVmgsResource::Reprovision(disk) => disk,
675 PetriVmgsResource::Ephemeral => PetriVmgsDisk::default(),
676 };
677 self.config.vmgs = match guest_state_lifetime {
678 PetriGuestStateLifetime::Disk => PetriVmgsResource::Disk(disk),
679 PetriGuestStateLifetime::ReprovisionOnFailure => {
680 PetriVmgsResource::ReprovisionOnFailure(disk)
681 }
682 PetriGuestStateLifetime::Reprovision => PetriVmgsResource::Reprovision(disk),
683 PetriGuestStateLifetime::Ephemeral => {
684 if !matches!(disk.disk, PetriDiskType::Memory) {
685 panic!("attempted to use ephemeral guest state after specifying backing vmgs")
686 }
687 PetriVmgsResource::Ephemeral
688 }
689 };
690 self
691 }
692
693 pub fn with_guest_state_encryption(mut self, policy: GuestStateEncryptionPolicy) -> Self {
695 match &mut self.config.vmgs {
696 PetriVmgsResource::Disk(vmgs)
697 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
698 | PetriVmgsResource::Reprovision(vmgs) => {
699 vmgs.encryption_policy = policy;
700 }
701 PetriVmgsResource::Ephemeral => {
702 panic!("attempted to encrypt ephemeral guest state")
703 }
704 }
705 self
706 }
707
708 pub fn with_initial_vmgs(self, disk: ResolvedArtifact<impl IsTestVmgs>) -> Self {
710 self.with_backing_vmgs(PetriDiskType::Differencing(disk.into()))
711 }
712
713 pub fn with_persistent_vmgs(self, disk: impl AsRef<Path>) -> Self {
715 self.with_backing_vmgs(PetriDiskType::Persistent(disk.as_ref().to_path_buf()))
716 }
717
718 fn with_backing_vmgs(mut self, disk: PetriDiskType) -> Self {
719 match &mut self.config.vmgs {
720 PetriVmgsResource::Disk(vmgs)
721 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
722 | PetriVmgsResource::Reprovision(vmgs) => {
723 if !matches!(vmgs.disk, PetriDiskType::Memory) {
724 panic!("already specified a backing vmgs file");
725 }
726 vmgs.disk = disk;
727 }
728 PetriVmgsResource::Ephemeral => {
729 panic!("attempted to specify a backing vmgs with ephemeral guest state")
730 }
731 }
732 self
733 }
734
735 pub fn with_boot_device_type(mut self, boot: BootDeviceType) -> Self {
739 self.config.boot_device_type = boot;
740 self
741 }
742
743 pub fn with_tpm_state_persistence(mut self, tpm_state_persistence: bool) -> Self {
745 self.config.tpm_state_persistence = tpm_state_persistence;
746 self
747 }
748
749 pub fn os_flavor(&self) -> OsFlavor {
751 self.config.firmware.os_flavor()
752 }
753
754 pub fn is_openhcl(&self) -> bool {
756 self.config.firmware.is_openhcl()
757 }
758
759 pub fn isolation(&self) -> Option<IsolationType> {
761 self.config.firmware.isolation()
762 }
763
764 pub fn arch(&self) -> MachineArch {
766 self.config.arch
767 }
768
769 pub fn default_servicing_flags(&self) -> OpenHclServicingFlags {
771 T::default_servicing_flags()
772 }
773
774 pub fn modify_backend(
776 mut self,
777 f: impl FnOnce(T::VmmConfig) -> T::VmmConfig + 'static + Send,
778 ) -> Self {
779 if self.modify_vmm_config.is_some() {
780 panic!("only one modify_backend allowed");
781 }
782 self.modify_vmm_config = Some(Box::new(f));
783 self
784 }
785}
786
787impl<T: PetriVmmBackend> PetriVm<T> {
788 pub async fn teardown(self) -> anyhow::Result<()> {
790 tracing::info!("Tearing down VM...");
791 self.runtime.teardown().await
792 }
793
794 pub async fn wait_for_halt(&mut self) -> anyhow::Result<PetriHaltReason> {
796 tracing::info!("Waiting for VM to halt...");
797 let halt_reason = self.runtime.wait_for_halt(false).await?;
798 tracing::info!("VM halted: {halt_reason:?}. Cancelling watchdogs...");
799 futures::future::join_all(self.watchdog_tasks.drain(..).map(|t| t.cancel())).await;
800 Ok(halt_reason)
801 }
802
803 pub async fn wait_for_clean_shutdown(&mut self) -> anyhow::Result<()> {
805 let halt_reason = self.wait_for_halt().await?;
806 if halt_reason != PetriHaltReason::PowerOff {
807 anyhow::bail!("Expected PowerOff, got {halt_reason:?}");
808 }
809 tracing::info!("VM was cleanly powered off and torn down.");
810 Ok(())
811 }
812
813 pub async fn wait_for_teardown(mut self) -> anyhow::Result<PetriHaltReason> {
816 let halt_reason = self.wait_for_halt().await?;
817 self.teardown().await?;
818 Ok(halt_reason)
819 }
820
821 pub async fn wait_for_clean_teardown(mut self) -> anyhow::Result<()> {
823 self.wait_for_clean_shutdown().await?;
824 self.teardown().await
825 }
826
827 pub async fn wait_for_reset_no_agent(&mut self) -> anyhow::Result<()> {
829 self.wait_for_reset_core().await?;
830 self.wait_for_expected_boot_event().await?;
831 Ok(())
832 }
833
834 pub async fn wait_for_reset(&mut self) -> anyhow::Result<PipetteClient> {
836 self.wait_for_reset_no_agent().await?;
837 self.wait_for_agent().await
838 }
839
840 async fn wait_for_reset_core(&mut self) -> anyhow::Result<()> {
841 tracing::info!("Waiting for VM to reset...");
842 let halt_reason = self.runtime.wait_for_halt(true).await?;
843 if halt_reason != PetriHaltReason::Reset {
844 anyhow::bail!("Expected reset, got {halt_reason:?}");
845 }
846 tracing::info!("VM reset.");
847 Ok(())
848 }
849
850 pub async fn inspect_openhcl(
861 &self,
862 path: impl Into<String>,
863 depth: Option<usize>,
864 timeout: Option<Duration>,
865 ) -> anyhow::Result<inspect::Node> {
866 self.openhcl_diag()?
867 .inspect(path.into().as_str(), depth, timeout)
868 .await
869 }
870
871 pub async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()> {
873 self.inspect_openhcl("", None, None).await.map(|_| ())
874 }
875
876 pub async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()> {
882 self.openhcl_diag()?.wait_for_vtl2().await
883 }
884
885 pub async fn kmsg(&self) -> anyhow::Result<diag_client::kmsg_stream::KmsgStream> {
887 self.openhcl_diag()?.kmsg().await
888 }
889
890 pub async fn openhcl_core_dump(&self, name: &str, path: &Path) -> anyhow::Result<()> {
893 self.openhcl_diag()?.core_dump(name, path).await
894 }
895
896 pub async fn openhcl_crash(&self, name: &str) -> anyhow::Result<()> {
898 self.openhcl_diag()?.crash(name).await
899 }
900
901 async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient> {
904 self.runtime.wait_for_agent(false).await
905 }
906
907 pub async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient> {
911 self.launch_vtl2_pipette().await?;
913 self.runtime.wait_for_agent(true).await
914 }
915
916 async fn wait_for_expected_boot_event(&mut self) -> anyhow::Result<()> {
923 if let Some(expected_event) = self.expected_boot_event {
924 let event = self.wait_for_boot_event().await?;
925
926 anyhow::ensure!(
927 event == expected_event,
928 "Did not receive expected boot event"
929 );
930 } else {
931 tracing::warn!("Boot event not emitted for configured firmware or manually ignored.");
932 }
933
934 Ok(())
935 }
936
937 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
940 tracing::info!("Waiting for boot event...");
941 let boot_event = loop {
942 match CancelContext::new()
943 .with_timeout(self.vmm_quirks.flaky_boot.unwrap_or(Duration::MAX))
944 .until_cancelled(self.runtime.wait_for_boot_event())
945 .await
946 {
947 Ok(res) => break res?,
948 Err(_) => {
949 tracing::error!("Did not get boot event in required time, resetting...");
950 if let Some(inspector) = self.runtime.inspector() {
951 save_inspect(
952 "vmm",
953 Box::pin(async move { inspector.inspect_all().await }),
954 &self.resources.log_source,
955 )
956 .await;
957 }
958
959 self.runtime.reset().await?;
960 continue;
961 }
962 }
963 };
964 tracing::info!("Got boot event: {boot_event:?}");
965 Ok(boot_event)
966 }
967
968 pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
971 tracing::info!("Waiting for enlightened shutdown to be ready");
972 self.runtime.wait_for_enlightened_shutdown_ready().await?;
973
974 let mut wait_time = Duration::from_secs(10);
980
981 if let Some(duration) = self.guest_quirks.hyperv_shutdown_ic_sleep {
983 wait_time += duration;
984 }
985
986 tracing::info!(
987 "Shutdown IC reported ready, waiting for an extra {}s",
988 wait_time.as_secs()
989 );
990 PolledTimer::new(&self.resources.driver)
991 .sleep(wait_time)
992 .await;
993
994 tracing::info!("Sending enlightened shutdown command");
995 self.runtime.send_enlightened_shutdown(kind).await
996 }
997
998 pub async fn restart_openhcl(
1001 &mut self,
1002 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1003 flags: OpenHclServicingFlags,
1004 ) -> anyhow::Result<()> {
1005 self.runtime
1006 .restart_openhcl(&new_openhcl.erase(), flags)
1007 .await
1008 }
1009
1010 pub async fn save_openhcl(
1013 &mut self,
1014 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1015 flags: OpenHclServicingFlags,
1016 ) -> anyhow::Result<()> {
1017 self.runtime.save_openhcl(&new_openhcl.erase(), flags).await
1018 }
1019
1020 pub async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
1023 self.runtime.restore_openhcl().await
1024 }
1025
1026 pub fn arch(&self) -> MachineArch {
1028 self.arch
1029 }
1030
1031 pub fn backend(&mut self) -> &mut T::VmRuntime {
1033 &mut self.runtime
1034 }
1035
1036 async fn launch_vtl2_pipette(&self) -> anyhow::Result<()> {
1037 tracing::debug!("Launching VTL 2 pipette...");
1038
1039 let res = self
1041 .openhcl_diag()?
1042 .run_vtl2_command("sh", &["-c", "mkdir /cidata && mount LABEL=cidata /cidata"])
1043 .await?;
1044
1045 if !res.exit_status.success() {
1046 anyhow::bail!("Failed to mount VTL 2 pipette drive: {:?}", res);
1047 }
1048
1049 let res = self
1050 .openhcl_diag()?
1051 .run_detached_vtl2_command("sh", &["-c", "/cidata/pipette 2>&1 | logger &"])
1052 .await?;
1053
1054 if !res.success() {
1055 anyhow::bail!("Failed to spawn VTL 2 pipette: {:?}", res);
1056 }
1057
1058 Ok(())
1059 }
1060
1061 fn openhcl_diag(&self) -> anyhow::Result<&OpenHclDiagHandler> {
1062 if let Some(ohd) = self.openhcl_diag_handler.as_ref() {
1063 Ok(ohd)
1064 } else {
1065 anyhow::bail!("VM is not configured with OpenHCL")
1066 }
1067 }
1068
1069 pub async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
1071 self.runtime.get_guest_state_file().await
1072 }
1073}
1074
1075#[async_trait]
1077pub trait PetriVmRuntime: Send + Sync + 'static {
1078 type VmInspector: PetriVmInspector;
1080 type VmFramebufferAccess: PetriVmFramebufferAccess;
1082
1083 async fn teardown(self) -> anyhow::Result<()>;
1085 async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReason>;
1088 async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>;
1090 fn openhcl_diag(&self) -> Option<OpenHclDiagHandler>;
1092 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent>;
1095 async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()>;
1098 async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
1100 async fn restart_openhcl(
1103 &mut self,
1104 new_openhcl: &ResolvedArtifact,
1105 flags: OpenHclServicingFlags,
1106 ) -> anyhow::Result<()>;
1107 async fn save_openhcl(
1111 &mut self,
1112 new_openhcl: &ResolvedArtifact,
1113 flags: OpenHclServicingFlags,
1114 ) -> anyhow::Result<()>;
1115 async fn restore_openhcl(&mut self) -> anyhow::Result<()>;
1118 fn inspector(&self) -> Option<Self::VmInspector> {
1120 None
1121 }
1122 fn take_framebuffer_access(&mut self) -> Option<Self::VmFramebufferAccess> {
1125 None
1126 }
1127 async fn reset(&mut self) -> anyhow::Result<()>;
1129 async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
1131 Ok(None)
1132 }
1133}
1134
1135#[async_trait]
1137pub trait PetriVmInspector: Send + Sync + 'static {
1138 async fn inspect_all(&self) -> anyhow::Result<inspect::Node>;
1140}
1141
1142pub struct NoPetriVmInspector;
1144#[async_trait]
1145impl PetriVmInspector for NoPetriVmInspector {
1146 async fn inspect_all(&self) -> anyhow::Result<inspect::Node> {
1147 unreachable!()
1148 }
1149}
1150
1151pub struct VmScreenshotMeta {
1153 pub color: image::ExtendedColorType,
1155 pub width: u16,
1157 pub height: u16,
1159}
1160
1161#[async_trait]
1163pub trait PetriVmFramebufferAccess: Send + 'static {
1164 async fn screenshot(&mut self, image: &mut Vec<u8>)
1167 -> anyhow::Result<Option<VmScreenshotMeta>>;
1168}
1169
1170pub struct NoPetriVmFramebufferAccess;
1172#[async_trait]
1173impl PetriVmFramebufferAccess for NoPetriVmFramebufferAccess {
1174 async fn screenshot(
1175 &mut self,
1176 _image: &mut Vec<u8>,
1177 ) -> anyhow::Result<Option<VmScreenshotMeta>> {
1178 unreachable!()
1179 }
1180}
1181
1182#[derive(Debug)]
1184pub struct ProcessorTopology {
1185 pub vp_count: u32,
1187 pub enable_smt: Option<bool>,
1189 pub vps_per_socket: Option<u32>,
1191 pub apic_mode: Option<ApicMode>,
1193}
1194
1195impl Default for ProcessorTopology {
1196 fn default() -> Self {
1197 Self {
1198 vp_count: 2,
1199 enable_smt: None,
1200 vps_per_socket: None,
1201 apic_mode: None,
1202 }
1203 }
1204}
1205
1206#[derive(Debug, Clone, Copy)]
1208pub enum ApicMode {
1209 Xapic,
1211 X2apicSupported,
1213 X2apicEnabled,
1215}
1216
1217pub struct MemoryConfig {
1219 pub startup_bytes: u64,
1222 pub dynamic_memory_range: Option<(u64, u64)>,
1226}
1227
1228impl Default for MemoryConfig {
1229 fn default() -> Self {
1230 Self {
1231 startup_bytes: 0x1_0000_0000,
1232 dynamic_memory_range: None,
1233 }
1234 }
1235}
1236
1237#[derive(Debug)]
1239pub struct UefiConfig {
1240 pub secure_boot_enabled: bool,
1242 pub secure_boot_template: Option<SecureBootTemplate>,
1244 pub disable_frontpage: bool,
1246 pub default_boot_always_attempt: bool,
1248}
1249
1250impl Default for UefiConfig {
1251 fn default() -> Self {
1252 Self {
1253 secure_boot_enabled: false,
1254 secure_boot_template: None,
1255 disable_frontpage: true,
1256 default_boot_always_attempt: false,
1257 }
1258 }
1259}
1260
1261#[derive(Debug, Clone)]
1263pub enum OpenHclLogConfig {
1264 TestDefault,
1268 BuiltInDefault,
1271 Custom(String),
1274}
1275
1276#[derive(Debug, Clone)]
1278pub struct OpenHclConfig {
1279 pub vtl2_nvme_boot: bool,
1282 pub vmbus_redirect: bool,
1284 pub command_line: Option<String>,
1288 pub log_levels: OpenHclLogConfig,
1292 pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
1295}
1296
1297impl OpenHclConfig {
1298 pub fn command_line(&self) -> String {
1301 let mut cmdline = self.command_line.clone();
1302 match &self.log_levels {
1303 OpenHclLogConfig::TestDefault => {
1304 let default_log_levels = {
1305 let openhcl_tracing = if let Ok(x) =
1307 std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG"))
1308 {
1309 format!("OPENVMM_LOG={x}")
1310 } else {
1311 "OPENVMM_LOG=debug".to_owned()
1312 };
1313 let openhcl_show_spans = if let Ok(x) = std::env::var("OPENVMM_SHOW_SPANS") {
1314 format!("OPENVMM_SHOW_SPANS={x}")
1315 } else {
1316 "OPENVMM_SHOW_SPANS=true".to_owned()
1317 };
1318 format!("{openhcl_tracing} {openhcl_show_spans}")
1319 };
1320 append_cmdline(&mut cmdline, &default_log_levels);
1321 }
1322 OpenHclLogConfig::BuiltInDefault => {
1323 }
1325 OpenHclLogConfig::Custom(levels) => {
1326 append_cmdline(&mut cmdline, levels);
1327 }
1328 }
1329
1330 cmdline.unwrap_or_default()
1331 }
1332}
1333
1334impl Default for OpenHclConfig {
1335 fn default() -> Self {
1336 Self {
1337 vtl2_nvme_boot: false,
1338 vmbus_redirect: false,
1339 command_line: None,
1340 log_levels: OpenHclLogConfig::TestDefault,
1341 vtl2_base_address_type: None,
1342 }
1343 }
1344}
1345
1346#[derive(Debug)]
1348pub enum Firmware {
1349 LinuxDirect {
1351 kernel: ResolvedArtifact,
1353 initrd: ResolvedArtifact,
1355 },
1356 OpenhclLinuxDirect {
1358 igvm_path: ResolvedArtifact,
1360 openhcl_config: OpenHclConfig,
1362 },
1363 Pcat {
1365 guest: PcatGuest,
1367 bios_firmware: ResolvedOptionalArtifact,
1369 svga_firmware: ResolvedOptionalArtifact,
1371 },
1372 OpenhclPcat {
1374 guest: PcatGuest,
1376 igvm_path: ResolvedArtifact,
1378 bios_firmware: ResolvedOptionalArtifact,
1380 svga_firmware: ResolvedOptionalArtifact,
1382 openhcl_config: OpenHclConfig,
1384 },
1385 Uefi {
1387 guest: UefiGuest,
1389 uefi_firmware: ResolvedArtifact,
1391 uefi_config: UefiConfig,
1393 },
1394 OpenhclUefi {
1396 guest: UefiGuest,
1398 isolation: Option<IsolationType>,
1400 igvm_path: ResolvedArtifact,
1402 uefi_config: UefiConfig,
1404 openhcl_config: OpenHclConfig,
1406 },
1407}
1408
1409#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1411pub enum BootDeviceType {
1412 None,
1414 Ide,
1416 Scsi,
1418 Nvme,
1420}
1421
1422impl Firmware {
1423 pub fn linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
1425 use petri_artifacts_vmm_test::artifacts::loadable::*;
1426 match arch {
1427 MachineArch::X86_64 => Firmware::LinuxDirect {
1428 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_X64).erase(),
1429 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
1430 },
1431 MachineArch::Aarch64 => Firmware::LinuxDirect {
1432 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_AARCH64).erase(),
1433 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_AARCH64).erase(),
1434 },
1435 }
1436 }
1437
1438 pub fn openhcl_linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
1440 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
1441 match arch {
1442 MachineArch::X86_64 => Firmware::OpenhclLinuxDirect {
1443 igvm_path: resolver.require(LATEST_LINUX_DIRECT_TEST_X64).erase(),
1444 openhcl_config: Default::default(),
1445 },
1446 MachineArch::Aarch64 => todo!("Linux direct not yet supported on aarch64"),
1447 }
1448 }
1449
1450 pub fn pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
1452 use petri_artifacts_vmm_test::artifacts::loadable::*;
1453 Firmware::Pcat {
1454 guest,
1455 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
1456 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
1457 }
1458 }
1459
1460 pub fn uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch, guest: UefiGuest) -> Self {
1462 use petri_artifacts_vmm_test::artifacts::loadable::*;
1463 let uefi_firmware = match arch {
1464 MachineArch::X86_64 => resolver.require(UEFI_FIRMWARE_X64).erase(),
1465 MachineArch::Aarch64 => resolver.require(UEFI_FIRMWARE_AARCH64).erase(),
1466 };
1467 Firmware::Uefi {
1468 guest,
1469 uefi_firmware,
1470 uefi_config: Default::default(),
1471 }
1472 }
1473
1474 pub fn openhcl_uefi(
1476 resolver: &ArtifactResolver<'_>,
1477 arch: MachineArch,
1478 guest: UefiGuest,
1479 isolation: Option<IsolationType>,
1480 vtl2_nvme_boot: bool,
1481 ) -> Self {
1482 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
1483 let igvm_path = match arch {
1484 MachineArch::X86_64 if isolation.is_some() => resolver.require(LATEST_CVM_X64).erase(),
1485 MachineArch::X86_64 => resolver.require(LATEST_STANDARD_X64).erase(),
1486 MachineArch::Aarch64 => resolver.require(LATEST_STANDARD_AARCH64).erase(),
1487 };
1488 Firmware::OpenhclUefi {
1489 guest,
1490 isolation,
1491 igvm_path,
1492 uefi_config: Default::default(),
1493 openhcl_config: OpenHclConfig {
1494 vtl2_nvme_boot,
1495 ..Default::default()
1496 },
1497 }
1498 }
1499
1500 fn is_openhcl(&self) -> bool {
1501 match self {
1502 Firmware::OpenhclLinuxDirect { .. }
1503 | Firmware::OpenhclUefi { .. }
1504 | Firmware::OpenhclPcat { .. } => true,
1505 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => false,
1506 }
1507 }
1508
1509 fn isolation(&self) -> Option<IsolationType> {
1510 match self {
1511 Firmware::OpenhclUefi { isolation, .. } => *isolation,
1512 Firmware::LinuxDirect { .. }
1513 | Firmware::Pcat { .. }
1514 | Firmware::Uefi { .. }
1515 | Firmware::OpenhclLinuxDirect { .. }
1516 | Firmware::OpenhclPcat { .. } => None,
1517 }
1518 }
1519
1520 fn is_linux_direct(&self) -> bool {
1521 match self {
1522 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => true,
1523 Firmware::Pcat { .. }
1524 | Firmware::Uefi { .. }
1525 | Firmware::OpenhclUefi { .. }
1526 | Firmware::OpenhclPcat { .. } => false,
1527 }
1528 }
1529
1530 fn is_pcat(&self) -> bool {
1531 match self {
1532 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => true,
1533 Firmware::Uefi { .. }
1534 | Firmware::OpenhclUefi { .. }
1535 | Firmware::LinuxDirect { .. }
1536 | Firmware::OpenhclLinuxDirect { .. } => false,
1537 }
1538 }
1539
1540 fn os_flavor(&self) -> OsFlavor {
1541 match self {
1542 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => OsFlavor::Linux,
1543 Firmware::Uefi {
1544 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
1545 ..
1546 }
1547 | Firmware::OpenhclUefi {
1548 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
1549 ..
1550 } => OsFlavor::Uefi,
1551 Firmware::Pcat {
1552 guest: PcatGuest::Vhd(cfg),
1553 ..
1554 }
1555 | Firmware::OpenhclPcat {
1556 guest: PcatGuest::Vhd(cfg),
1557 ..
1558 }
1559 | Firmware::Uefi {
1560 guest: UefiGuest::Vhd(cfg),
1561 ..
1562 }
1563 | Firmware::OpenhclUefi {
1564 guest: UefiGuest::Vhd(cfg),
1565 ..
1566 } => cfg.os_flavor,
1567 Firmware::Pcat {
1568 guest: PcatGuest::Iso(cfg),
1569 ..
1570 }
1571 | Firmware::OpenhclPcat {
1572 guest: PcatGuest::Iso(cfg),
1573 ..
1574 } => cfg.os_flavor,
1575 }
1576 }
1577
1578 fn quirks(&self) -> GuestQuirks {
1579 match self {
1580 Firmware::Pcat {
1581 guest: PcatGuest::Vhd(cfg),
1582 ..
1583 }
1584 | Firmware::Uefi {
1585 guest: UefiGuest::Vhd(cfg),
1586 ..
1587 }
1588 | Firmware::OpenhclUefi {
1589 guest: UefiGuest::Vhd(cfg),
1590 ..
1591 } => cfg.quirks.clone(),
1592 Firmware::Pcat {
1593 guest: PcatGuest::Iso(cfg),
1594 ..
1595 } => cfg.quirks.clone(),
1596 _ => Default::default(),
1597 }
1598 }
1599
1600 fn expected_boot_event(&self) -> Option<FirmwareEvent> {
1601 match self {
1602 Firmware::LinuxDirect { .. }
1603 | Firmware::OpenhclLinuxDirect { .. }
1604 | Firmware::Uefi {
1605 guest: UefiGuest::GuestTestUefi(_),
1606 ..
1607 }
1608 | Firmware::OpenhclUefi {
1609 guest: UefiGuest::GuestTestUefi(_),
1610 ..
1611 } => None,
1612 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => {
1613 Some(FirmwareEvent::BootAttempt)
1615 }
1616 Firmware::Uefi {
1617 guest: UefiGuest::None,
1618 ..
1619 }
1620 | Firmware::OpenhclUefi {
1621 guest: UefiGuest::None,
1622 ..
1623 } => Some(FirmwareEvent::NoBootDevice),
1624 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => {
1625 Some(FirmwareEvent::BootSuccess)
1626 }
1627 }
1628 }
1629
1630 fn openhcl_config(&self) -> Option<&OpenHclConfig> {
1631 match self {
1632 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
1633 | Firmware::OpenhclUefi { openhcl_config, .. }
1634 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
1635 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
1636 }
1637 }
1638
1639 fn openhcl_config_mut(&mut self) -> Option<&mut OpenHclConfig> {
1640 match self {
1641 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
1642 | Firmware::OpenhclUefi { openhcl_config, .. }
1643 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
1644 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
1645 }
1646 }
1647
1648 fn uefi_config(&self) -> Option<&UefiConfig> {
1649 match self {
1650 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
1651 Some(uefi_config)
1652 }
1653 Firmware::LinuxDirect { .. }
1654 | Firmware::OpenhclLinuxDirect { .. }
1655 | Firmware::Pcat { .. }
1656 | Firmware::OpenhclPcat { .. } => None,
1657 }
1658 }
1659
1660 fn uefi_config_mut(&mut self) -> Option<&mut UefiConfig> {
1661 match self {
1662 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
1663 Some(uefi_config)
1664 }
1665 Firmware::LinuxDirect { .. }
1666 | Firmware::OpenhclLinuxDirect { .. }
1667 | Firmware::Pcat { .. }
1668 | Firmware::OpenhclPcat { .. } => None,
1669 }
1670 }
1671}
1672
1673#[derive(Debug)]
1676pub enum PcatGuest {
1677 Vhd(BootImageConfig<boot_image_type::Vhd>),
1679 Iso(BootImageConfig<boot_image_type::Iso>),
1681}
1682
1683impl PcatGuest {
1684 fn artifact(&self) -> &ResolvedArtifact {
1685 match self {
1686 PcatGuest::Vhd(disk) => &disk.artifact,
1687 PcatGuest::Iso(disk) => &disk.artifact,
1688 }
1689 }
1690}
1691
1692#[derive(Debug)]
1695pub enum UefiGuest {
1696 Vhd(BootImageConfig<boot_image_type::Vhd>),
1698 GuestTestUefi(ResolvedArtifact),
1700 None,
1702}
1703
1704impl UefiGuest {
1705 pub fn guest_test_uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
1707 use petri_artifacts_vmm_test::artifacts::test_vhd::*;
1708 let artifact = match arch {
1709 MachineArch::X86_64 => resolver.require(GUEST_TEST_UEFI_X64).erase(),
1710 MachineArch::Aarch64 => resolver.require(GUEST_TEST_UEFI_AARCH64).erase(),
1711 };
1712 UefiGuest::GuestTestUefi(artifact)
1713 }
1714
1715 fn artifact(&self) -> Option<&ResolvedArtifact> {
1716 match self {
1717 UefiGuest::Vhd(vhd) => Some(&vhd.artifact),
1718 UefiGuest::GuestTestUefi(p) => Some(p),
1719 UefiGuest::None => None,
1720 }
1721 }
1722}
1723
1724pub mod boot_image_type {
1726 mod private {
1727 pub trait Sealed {}
1728 impl Sealed for super::Vhd {}
1729 impl Sealed for super::Iso {}
1730 }
1731
1732 pub trait BootImageType: private::Sealed {}
1735
1736 #[derive(Debug)]
1738 pub enum Vhd {}
1739
1740 #[derive(Debug)]
1742 pub enum Iso {}
1743
1744 impl BootImageType for Vhd {}
1745 impl BootImageType for Iso {}
1746}
1747
1748#[derive(Debug)]
1750pub struct BootImageConfig<T: boot_image_type::BootImageType> {
1751 artifact: ResolvedArtifact,
1753 os_flavor: OsFlavor,
1755 quirks: GuestQuirks,
1759 _type: core::marker::PhantomData<T>,
1761}
1762
1763impl BootImageConfig<boot_image_type::Vhd> {
1764 pub fn from_vhd<A>(artifact: ResolvedArtifact<A>) -> Self
1766 where
1767 A: petri_artifacts_common::tags::IsTestVhd,
1768 {
1769 BootImageConfig {
1770 artifact: artifact.erase(),
1771 os_flavor: A::OS_FLAVOR,
1772 quirks: A::quirks(),
1773 _type: std::marker::PhantomData,
1774 }
1775 }
1776}
1777
1778impl BootImageConfig<boot_image_type::Iso> {
1779 pub fn from_iso<A>(artifact: ResolvedArtifact<A>) -> Self
1781 where
1782 A: petri_artifacts_common::tags::IsTestIso,
1783 {
1784 BootImageConfig {
1785 artifact: artifact.erase(),
1786 os_flavor: A::OS_FLAVOR,
1787 quirks: A::quirks(),
1788 _type: std::marker::PhantomData,
1789 }
1790 }
1791}
1792
1793#[derive(Debug, Clone, Copy)]
1795pub enum IsolationType {
1796 Vbs,
1798 Snp,
1800 Tdx,
1802}
1803
1804#[derive(Debug, Clone, Copy)]
1806pub struct OpenHclServicingFlags {
1807 pub enable_nvme_keepalive: bool,
1810 pub override_version_checks: bool,
1812 pub stop_timeout_hint_secs: Option<u16>,
1814}
1815
1816#[derive(Debug, Clone)]
1818pub enum PetriDiskType {
1819 Memory,
1821 Differencing(PathBuf),
1823 Persistent(PathBuf),
1825}
1826
1827#[derive(Debug, Clone)]
1829pub struct PetriVmgsDisk {
1830 pub disk: PetriDiskType,
1832 pub encryption_policy: GuestStateEncryptionPolicy,
1834}
1835
1836impl Default for PetriVmgsDisk {
1837 fn default() -> Self {
1838 PetriVmgsDisk {
1839 disk: PetriDiskType::Memory,
1840 encryption_policy: GuestStateEncryptionPolicy::None(false),
1842 }
1843 }
1844}
1845
1846#[derive(Debug, Clone)]
1848pub enum PetriVmgsResource {
1849 Disk(PetriVmgsDisk),
1851 ReprovisionOnFailure(PetriVmgsDisk),
1853 Reprovision(PetriVmgsDisk),
1855 Ephemeral,
1857}
1858
1859impl PetriVmgsResource {
1860 pub fn disk(&self) -> Option<&PetriVmgsDisk> {
1862 match self {
1863 PetriVmgsResource::Disk(vmgs)
1864 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1865 | PetriVmgsResource::Reprovision(vmgs) => Some(vmgs),
1866 PetriVmgsResource::Ephemeral => None,
1867 }
1868 }
1869}
1870
1871#[derive(Debug, Clone, Copy)]
1873pub enum PetriGuestStateLifetime {
1874 Disk,
1877 ReprovisionOnFailure,
1879 Reprovision,
1881 Ephemeral,
1883}
1884
1885#[derive(Debug, Clone, Copy)]
1887pub enum SecureBootTemplate {
1888 MicrosoftWindows,
1890 MicrosoftUefiCertificateAuthority,
1892}
1893
1894#[derive(Default, Debug, Clone)]
1897pub struct VmmQuirks {
1898 pub flaky_boot: Option<Duration>,
1901}
1902
1903fn make_vm_safe_name(name: &str) -> String {
1909 const MAX_VM_NAME_LENGTH: usize = 100;
1910 const HASH_LENGTH: usize = 4;
1911 const MAX_PREFIX_LENGTH: usize = MAX_VM_NAME_LENGTH - HASH_LENGTH;
1912
1913 if name.len() <= MAX_VM_NAME_LENGTH {
1914 name.to_owned()
1915 } else {
1916 let mut hasher = DefaultHasher::new();
1918 name.hash(&mut hasher);
1919 let hash = hasher.finish();
1920
1921 let hash_suffix = format!("{:04x}", hash & 0xFFFF);
1923
1924 let truncated = &name[..MAX_PREFIX_LENGTH];
1926 tracing::debug!(
1927 "VM name too long ({}), truncating '{}' to '{}{}'",
1928 name.len(),
1929 name,
1930 truncated,
1931 hash_suffix
1932 );
1933
1934 format!("{}{}", truncated, hash_suffix)
1935 }
1936}
1937
1938#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1940pub enum PetriHaltReason {
1941 PowerOff,
1943 Reset,
1945 Hibernate,
1947 TripleFault,
1949 Other,
1951}
1952
1953fn append_cmdline(cmd: &mut Option<String>, add_cmd: impl AsRef<str>) {
1954 if let Some(cmd) = cmd.as_mut() {
1955 cmd.push(' ');
1956 cmd.push_str(add_cmd.as_ref());
1957 } else {
1958 *cmd = Some(add_cmd.as_ref().to_string());
1959 }
1960}
1961
1962async fn save_inspect(
1963 name: &str,
1964 inspect: std::pin::Pin<Box<dyn Future<Output = anyhow::Result<inspect::Node>> + Send>>,
1965 log_source: &PetriLogSource,
1966) {
1967 tracing::info!("Collecting {name} inspect details.");
1968 let node = match inspect.await {
1969 Ok(n) => n,
1970 Err(e) => {
1971 tracing::error!(?e, "Failed to get {name}");
1972 return;
1973 }
1974 };
1975 if let Err(e) = log_source.write_attachment(
1976 &format!("timeout_inspect_{name}.log"),
1977 format!("{node:#}").as_bytes(),
1978 ) {
1979 tracing::error!(?e, "Failed to save {name} inspect log");
1980 return;
1981 }
1982 tracing::info!("{name} inspect task finished.");
1983}
1984
1985#[cfg(test)]
1986mod tests {
1987 use super::make_vm_safe_name;
1988
1989 #[test]
1990 fn test_short_names_unchanged() {
1991 let short_name = "short_test_name";
1992 assert_eq!(make_vm_safe_name(short_name), short_name);
1993 }
1994
1995 #[test]
1996 fn test_exactly_100_chars_unchanged() {
1997 let name_100 = "a".repeat(100);
1998 assert_eq!(make_vm_safe_name(&name_100), name_100);
1999 }
2000
2001 #[test]
2002 fn test_long_name_truncated() {
2003 let long_name = "multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_openhcl_servicing";
2004 let result = make_vm_safe_name(long_name);
2005
2006 assert_eq!(result.len(), 100);
2008
2009 assert!(result.starts_with("multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_ope"));
2011
2012 let suffix = &result[96..];
2014 assert_eq!(suffix.len(), 4);
2015 assert!(u16::from_str_radix(suffix, 16).is_ok());
2017 }
2018
2019 #[test]
2020 fn test_deterministic_results() {
2021 let long_name = "very_long_test_name_that_exceeds_the_100_character_limit_and_should_be_truncated_consistently_every_time";
2022 let result1 = make_vm_safe_name(long_name);
2023 let result2 = make_vm_safe_name(long_name);
2024
2025 assert_eq!(result1, result2);
2026 assert_eq!(result1.len(), 100);
2027 }
2028
2029 #[test]
2030 fn test_different_names_different_hashes() {
2031 let name1 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_1";
2032 let name2 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_2";
2033
2034 let result1 = make_vm_safe_name(name1);
2035 let result2 = make_vm_safe_name(name2);
2036
2037 assert_eq!(result1.len(), 100);
2039 assert_eq!(result2.len(), 100);
2040
2041 assert_ne!(result1, result2);
2043 assert_ne!(&result1[96..], &result2[96..]);
2044 }
2045}