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: Option<TpmConfig>,
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: None,
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
358 || matches!(
359 (
360 self.guest_quirks.initial_reboot,
361 self.expected_boot_event,
362 &self.config.firmware,
363 &self.config.tpm,
364 ),
365 (
366 Some(InitialRebootCondition::Always),
367 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
368 _,
369 _,
370 ) | (
371 Some(InitialRebootCondition::WithTpm),
372 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
373 _,
374 Some(_),
375 )
376 )
377 }
378
379 fn start_watchdog_tasks(
380 resources: &PetriVmResources,
381 runtime: &mut T::VmRuntime,
382 ) -> anyhow::Result<Vec<Task<()>>> {
383 let mut tasks = Vec::new();
384
385 {
386 const TIMEOUT_DURATION_MINUTES: u64 = 10;
387 const TIMER_DURATION: Duration = Duration::from_secs(TIMEOUT_DURATION_MINUTES * 60);
388 let log_source = resources.log_source.clone();
389 let inspect_task =
390 |name,
391 driver: &DefaultDriver,
392 inspect: std::pin::Pin<Box<dyn Future<Output = _> + Send>>| {
393 driver.spawn(format!("petri-watchdog-inspect-{name}"), async move {
394 if CancelContext::new()
395 .with_timeout(Duration::from_secs(10))
396 .until_cancelled(save_inspect(name, inspect, &log_source))
397 .await
398 .is_err()
399 {
400 tracing::warn!(name, "Failed to collect inspect data within timeout");
401 }
402 })
403 };
404
405 let driver = resources.driver.clone();
406 let vmm_inspector = runtime.inspector();
407 let openhcl_diag_handler = runtime.openhcl_diag();
408 tasks.push(resources.driver.spawn("timer-watchdog", async move {
409 PolledTimer::new(&driver).sleep(TIMER_DURATION).await;
410 tracing::warn!("Test timeout reached after {TIMEOUT_DURATION_MINUTES} minutes, collecting diagnostics.");
411 let mut timeout_tasks = Vec::new();
412 if let Some(inspector) = vmm_inspector {
413 timeout_tasks.push(inspect_task.clone()("vmm", &driver, Box::pin(async move { inspector.inspect_all().await })) );
414 }
415 if let Some(openhcl_diag_handler) = openhcl_diag_handler {
416 timeout_tasks.push(inspect_task("openhcl", &driver, Box::pin(async move { openhcl_diag_handler.inspect("", None, None).await })));
417 }
418 futures::future::join_all(timeout_tasks).await;
419 tracing::error!("Test time out diagnostics collection complete, aborting.");
420 panic!("Test timed out");
421 }));
422 }
423
424 if let Some(mut framebuffer_access) = runtime.take_framebuffer_access() {
425 let mut timer = PolledTimer::new(&resources.driver);
426 let log_source = resources.log_source.clone();
427
428 tasks.push(
429 resources
430 .driver
431 .spawn("petri-watchdog-screenshot", async move {
432 let mut image = Vec::new();
433 let mut last_image = Vec::new();
434 loop {
435 timer.sleep(Duration::from_secs(2)).await;
436 tracing::trace!("Taking screenshot.");
437
438 let VmScreenshotMeta {
439 color,
440 width,
441 height,
442 } = match framebuffer_access.screenshot(&mut image).await {
443 Ok(Some(meta)) => meta,
444 Ok(None) => {
445 tracing::debug!("VM off, skipping screenshot.");
446 continue;
447 }
448 Err(e) => {
449 tracing::error!(?e, "Failed to take screenshot");
450 continue;
451 }
452 };
453
454 if image == last_image {
455 tracing::debug!("No change in framebuffer, skipping screenshot.");
456 continue;
457 }
458
459 let r =
460 log_source
461 .create_attachment("screenshot.png")
462 .and_then(|mut f| {
463 image::write_buffer_with_format(
464 &mut f,
465 &image,
466 width.into(),
467 height.into(),
468 color,
469 image::ImageFormat::Png,
470 )
471 .map_err(Into::into)
472 });
473
474 if let Err(e) = r {
475 tracing::error!(?e, "Failed to save screenshot");
476 } else {
477 tracing::info!("Screenshot saved.");
478 }
479
480 std::mem::swap(&mut image, &mut last_image);
481 }
482 }),
483 );
484 }
485
486 Ok(tasks)
487 }
488
489 pub fn with_expect_boot_failure(mut self) -> Self {
492 self.expected_boot_event = Some(FirmwareEvent::BootFailed);
493 self
494 }
495
496 pub fn with_expect_no_boot_event(mut self) -> Self {
499 self.expected_boot_event = None;
500 self
501 }
502
503 pub fn with_expect_reset(mut self) -> Self {
507 self.override_expect_reset = true;
508 self
509 }
510
511 pub fn with_secure_boot(mut self) -> Self {
513 self.config
514 .firmware
515 .uefi_config_mut()
516 .expect("Secure boot is only supported for UEFI firmware.")
517 .secure_boot_enabled = true;
518
519 match self.os_flavor() {
520 OsFlavor::Windows => self.with_windows_secure_boot_template(),
521 OsFlavor::Linux => self.with_uefi_ca_secure_boot_template(),
522 _ => panic!(
523 "Secure boot unsupported for OS flavor {:?}",
524 self.os_flavor()
525 ),
526 }
527 }
528
529 pub fn with_windows_secure_boot_template(mut self) -> Self {
531 self.config
532 .firmware
533 .uefi_config_mut()
534 .expect("Secure boot is only supported for UEFI firmware.")
535 .secure_boot_template = Some(SecureBootTemplate::MicrosoftWindows);
536 self
537 }
538
539 pub fn with_uefi_ca_secure_boot_template(mut self) -> Self {
541 self.config
542 .firmware
543 .uefi_config_mut()
544 .expect("Secure boot is only supported for UEFI firmware.")
545 .secure_boot_template = Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority);
546 self
547 }
548
549 pub fn with_processor_topology(mut self, topology: ProcessorTopology) -> Self {
551 self.config.proc_topology = topology;
552 self
553 }
554
555 pub fn with_memory(mut self, memory: MemoryConfig) -> Self {
557 self.config.memory = memory;
558 self
559 }
560
561 pub fn with_vtl2_base_address_type(mut self, address_type: Vtl2BaseAddressType) -> Self {
566 self.config
567 .firmware
568 .openhcl_config_mut()
569 .expect("OpenHCL firmware is required to set custom VTL2 address type.")
570 .vtl2_base_address_type = Some(address_type);
571 self
572 }
573
574 pub fn with_custom_openhcl(mut self, artifact: ResolvedArtifact<impl IsOpenhclIgvm>) -> Self {
576 match &mut self.config.firmware {
577 Firmware::OpenhclLinuxDirect { igvm_path, .. }
578 | Firmware::OpenhclPcat { igvm_path, .. }
579 | Firmware::OpenhclUefi { igvm_path, .. } => {
580 *igvm_path = artifact.erase();
581 }
582 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } | Firmware::Pcat { .. } => {
583 panic!("Custom OpenHCL is only supported for OpenHCL firmware.")
584 }
585 }
586 self
587 }
588
589 pub fn with_openhcl_command_line(mut self, additional_command_line: &str) -> Self {
591 append_cmdline(
592 &mut self
593 .config
594 .firmware
595 .openhcl_config_mut()
596 .expect("OpenHCL command line is only supported for OpenHCL firmware.")
597 .command_line,
598 additional_command_line,
599 );
600 self
601 }
602
603 pub fn with_confidential_filtering(self) -> Self {
605 if !self.config.firmware.is_openhcl() {
606 panic!("Confidential filtering is only supported for OpenHCL");
607 }
608 self.with_openhcl_command_line(&format!(
609 "{}=1 {}=0",
610 underhill_confidentiality::OPENHCL_CONFIDENTIAL_ENV_VAR_NAME,
611 underhill_confidentiality::OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME
612 ))
613 }
614
615 pub fn with_openhcl_log_levels(mut self, levels: OpenHclLogConfig) -> Self {
617 self.config
618 .firmware
619 .openhcl_config_mut()
620 .expect("OpenHCL firmware is required to set custom OpenHCL log levels.")
621 .log_levels = levels;
622 self
623 }
624
625 pub fn with_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
627 self.config
628 .agent_image
629 .as_mut()
630 .expect("no guest pipette")
631 .add_file(name, artifact);
632 self
633 }
634
635 pub fn with_openhcl_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
637 self.config
638 .openhcl_agent_image
639 .as_mut()
640 .expect("no openhcl pipette")
641 .add_file(name, artifact);
642 self
643 }
644
645 pub fn with_uefi_frontpage(mut self, enable: bool) -> Self {
647 self.config
648 .firmware
649 .uefi_config_mut()
650 .expect("UEFI frontpage is only supported for UEFI firmware.")
651 .disable_frontpage = !enable;
652 self
653 }
654
655 pub fn with_default_boot_always_attempt(mut self, enable: bool) -> Self {
657 self.config
658 .firmware
659 .uefi_config_mut()
660 .expect("Default boot always attempt is only supported for UEFI firmware.")
661 .default_boot_always_attempt = enable;
662 self
663 }
664
665 pub fn with_vmbus_redirect(mut self, enable: bool) -> Self {
667 self.config
668 .firmware
669 .openhcl_config_mut()
670 .expect("VMBus redirection is only supported for OpenHCL firmware.")
671 .vmbus_redirect = enable;
672 self
673 }
674
675 pub fn with_guest_state_lifetime(
677 mut self,
678 guest_state_lifetime: PetriGuestStateLifetime,
679 ) -> Self {
680 let disk = match self.config.vmgs {
681 PetriVmgsResource::Disk(disk)
682 | PetriVmgsResource::ReprovisionOnFailure(disk)
683 | PetriVmgsResource::Reprovision(disk) => disk,
684 PetriVmgsResource::Ephemeral => PetriVmgsDisk::default(),
685 };
686 self.config.vmgs = match guest_state_lifetime {
687 PetriGuestStateLifetime::Disk => PetriVmgsResource::Disk(disk),
688 PetriGuestStateLifetime::ReprovisionOnFailure => {
689 PetriVmgsResource::ReprovisionOnFailure(disk)
690 }
691 PetriGuestStateLifetime::Reprovision => PetriVmgsResource::Reprovision(disk),
692 PetriGuestStateLifetime::Ephemeral => {
693 if !matches!(disk.disk, PetriDiskType::Memory) {
694 panic!("attempted to use ephemeral guest state after specifying backing vmgs")
695 }
696 PetriVmgsResource::Ephemeral
697 }
698 };
699 self
700 }
701
702 pub fn with_guest_state_encryption(mut self, policy: GuestStateEncryptionPolicy) -> Self {
704 match &mut self.config.vmgs {
705 PetriVmgsResource::Disk(vmgs)
706 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
707 | PetriVmgsResource::Reprovision(vmgs) => {
708 vmgs.encryption_policy = policy;
709 }
710 PetriVmgsResource::Ephemeral => {
711 panic!("attempted to encrypt ephemeral guest state")
712 }
713 }
714 self
715 }
716
717 pub fn with_initial_vmgs(self, disk: ResolvedArtifact<impl IsTestVmgs>) -> Self {
719 self.with_backing_vmgs(PetriDiskType::Differencing(disk.into()))
720 }
721
722 pub fn with_persistent_vmgs(self, disk: impl AsRef<Path>) -> Self {
724 self.with_backing_vmgs(PetriDiskType::Persistent(disk.as_ref().to_path_buf()))
725 }
726
727 fn with_backing_vmgs(mut self, disk: PetriDiskType) -> Self {
728 match &mut self.config.vmgs {
729 PetriVmgsResource::Disk(vmgs)
730 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
731 | PetriVmgsResource::Reprovision(vmgs) => {
732 if !matches!(vmgs.disk, PetriDiskType::Memory) {
733 panic!("already specified a backing vmgs file");
734 }
735 vmgs.disk = disk;
736 }
737 PetriVmgsResource::Ephemeral => {
738 panic!("attempted to specify a backing vmgs with ephemeral guest state")
739 }
740 }
741 self
742 }
743
744 pub fn with_boot_device_type(mut self, boot: BootDeviceType) -> Self {
748 self.config.boot_device_type = boot;
749 self
750 }
751
752 pub fn with_tpm(mut self, enable: bool) -> Self {
754 if enable {
755 self.config.tpm.get_or_insert_default();
756 } else {
757 self.config.tpm = None;
758 }
759 self
760 }
761
762 pub fn with_tpm_state_persistence(mut self, tpm_state_persistence: bool) -> Self {
764 self.config
765 .tpm
766 .as_mut()
767 .expect("TPM persistence requires a TPM")
768 .no_persistent_secrets = !tpm_state_persistence;
769 self
770 }
771
772 pub fn os_flavor(&self) -> OsFlavor {
774 self.config.firmware.os_flavor()
775 }
776
777 pub fn is_openhcl(&self) -> bool {
779 self.config.firmware.is_openhcl()
780 }
781
782 pub fn isolation(&self) -> Option<IsolationType> {
784 self.config.firmware.isolation()
785 }
786
787 pub fn arch(&self) -> MachineArch {
789 self.config.arch
790 }
791
792 pub fn default_servicing_flags(&self) -> OpenHclServicingFlags {
794 T::default_servicing_flags()
795 }
796
797 pub fn modify_backend(
799 mut self,
800 f: impl FnOnce(T::VmmConfig) -> T::VmmConfig + 'static + Send,
801 ) -> Self {
802 if self.modify_vmm_config.is_some() {
803 panic!("only one modify_backend allowed");
804 }
805 self.modify_vmm_config = Some(Box::new(f));
806 self
807 }
808}
809
810impl<T: PetriVmmBackend> PetriVm<T> {
811 pub async fn teardown(self) -> anyhow::Result<()> {
813 tracing::info!("Tearing down VM...");
814 self.runtime.teardown().await
815 }
816
817 pub async fn wait_for_halt(&mut self) -> anyhow::Result<PetriHaltReason> {
819 tracing::info!("Waiting for VM to halt...");
820 let halt_reason = self.runtime.wait_for_halt(false).await?;
821 tracing::info!("VM halted: {halt_reason:?}. Cancelling watchdogs...");
822 futures::future::join_all(self.watchdog_tasks.drain(..).map(|t| t.cancel())).await;
823 Ok(halt_reason)
824 }
825
826 pub async fn wait_for_clean_shutdown(&mut self) -> anyhow::Result<()> {
828 let halt_reason = self.wait_for_halt().await?;
829 if halt_reason != PetriHaltReason::PowerOff {
830 anyhow::bail!("Expected PowerOff, got {halt_reason:?}");
831 }
832 tracing::info!("VM was cleanly powered off and torn down.");
833 Ok(())
834 }
835
836 pub async fn wait_for_teardown(mut self) -> anyhow::Result<PetriHaltReason> {
839 let halt_reason = self.wait_for_halt().await?;
840 self.teardown().await?;
841 Ok(halt_reason)
842 }
843
844 pub async fn wait_for_clean_teardown(mut self) -> anyhow::Result<()> {
846 self.wait_for_clean_shutdown().await?;
847 self.teardown().await
848 }
849
850 pub async fn wait_for_reset_no_agent(&mut self) -> anyhow::Result<()> {
852 self.wait_for_reset_core().await?;
853 self.wait_for_expected_boot_event().await?;
854 Ok(())
855 }
856
857 pub async fn wait_for_reset(&mut self) -> anyhow::Result<PipetteClient> {
859 self.wait_for_reset_no_agent().await?;
860 self.wait_for_agent().await
861 }
862
863 async fn wait_for_reset_core(&mut self) -> anyhow::Result<()> {
864 tracing::info!("Waiting for VM to reset...");
865 let halt_reason = self.runtime.wait_for_halt(true).await?;
866 if halt_reason != PetriHaltReason::Reset {
867 anyhow::bail!("Expected reset, got {halt_reason:?}");
868 }
869 tracing::info!("VM reset.");
870 Ok(())
871 }
872
873 pub async fn inspect_openhcl(
884 &self,
885 path: impl Into<String>,
886 depth: Option<usize>,
887 timeout: Option<Duration>,
888 ) -> anyhow::Result<inspect::Node> {
889 self.openhcl_diag()?
890 .inspect(path.into().as_str(), depth, timeout)
891 .await
892 }
893
894 pub async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()> {
896 self.inspect_openhcl("", None, None).await.map(|_| ())
897 }
898
899 pub async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()> {
905 self.openhcl_diag()?.wait_for_vtl2().await
906 }
907
908 pub async fn kmsg(&self) -> anyhow::Result<diag_client::kmsg_stream::KmsgStream> {
910 self.openhcl_diag()?.kmsg().await
911 }
912
913 pub async fn openhcl_core_dump(&self, name: &str, path: &Path) -> anyhow::Result<()> {
916 self.openhcl_diag()?.core_dump(name, path).await
917 }
918
919 pub async fn openhcl_crash(&self, name: &str) -> anyhow::Result<()> {
921 self.openhcl_diag()?.crash(name).await
922 }
923
924 async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient> {
927 self.runtime.wait_for_enlightened_shutdown_ready().await?;
934 self.runtime.wait_for_agent(false).await
935 }
936
937 pub async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient> {
941 self.launch_vtl2_pipette().await?;
943 self.runtime.wait_for_agent(true).await
944 }
945
946 async fn wait_for_expected_boot_event(&mut self) -> anyhow::Result<()> {
953 if let Some(expected_event) = self.expected_boot_event {
954 let event = self.wait_for_boot_event().await?;
955
956 anyhow::ensure!(
957 event == expected_event,
958 "Did not receive expected boot event"
959 );
960 } else {
961 tracing::warn!("Boot event not emitted for configured firmware or manually ignored.");
962 }
963
964 Ok(())
965 }
966
967 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
970 tracing::info!("Waiting for boot event...");
971 let boot_event = loop {
972 match CancelContext::new()
973 .with_timeout(self.vmm_quirks.flaky_boot.unwrap_or(Duration::MAX))
974 .until_cancelled(self.runtime.wait_for_boot_event())
975 .await
976 {
977 Ok(res) => break res?,
978 Err(_) => {
979 tracing::error!("Did not get boot event in required time, resetting...");
980 if let Some(inspector) = self.runtime.inspector() {
981 save_inspect(
982 "vmm",
983 Box::pin(async move { inspector.inspect_all().await }),
984 &self.resources.log_source,
985 )
986 .await;
987 }
988
989 self.runtime.reset().await?;
990 continue;
991 }
992 }
993 };
994 tracing::info!("Got boot event: {boot_event:?}");
995 Ok(boot_event)
996 }
997
998 pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
1001 tracing::info!("Waiting for enlightened shutdown to be ready");
1002 self.runtime.wait_for_enlightened_shutdown_ready().await?;
1003
1004 let mut wait_time = Duration::from_secs(10);
1010
1011 if let Some(duration) = self.guest_quirks.hyperv_shutdown_ic_sleep {
1013 wait_time += duration;
1014 }
1015
1016 tracing::info!(
1017 "Shutdown IC reported ready, waiting for an extra {}s",
1018 wait_time.as_secs()
1019 );
1020 PolledTimer::new(&self.resources.driver)
1021 .sleep(wait_time)
1022 .await;
1023
1024 tracing::info!("Sending enlightened shutdown command");
1025 self.runtime.send_enlightened_shutdown(kind).await
1026 }
1027
1028 pub async fn restart_openhcl(
1031 &mut self,
1032 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1033 flags: OpenHclServicingFlags,
1034 ) -> anyhow::Result<()> {
1035 self.runtime
1036 .restart_openhcl(&new_openhcl.erase(), flags)
1037 .await
1038 }
1039
1040 pub async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()> {
1043 self.runtime.update_command_line(command_line).await
1044 }
1045
1046 pub async fn save_openhcl(
1049 &mut self,
1050 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
1051 flags: OpenHclServicingFlags,
1052 ) -> anyhow::Result<()> {
1053 self.runtime.save_openhcl(&new_openhcl.erase(), flags).await
1054 }
1055
1056 pub async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
1059 self.runtime.restore_openhcl().await
1060 }
1061
1062 pub fn arch(&self) -> MachineArch {
1064 self.arch
1065 }
1066
1067 pub fn backend(&mut self) -> &mut T::VmRuntime {
1069 &mut self.runtime
1070 }
1071
1072 async fn launch_vtl2_pipette(&self) -> anyhow::Result<()> {
1073 tracing::debug!("Launching VTL 2 pipette...");
1074
1075 let res = self
1077 .openhcl_diag()?
1078 .run_vtl2_command("sh", &["-c", "mkdir /cidata && mount LABEL=cidata /cidata"])
1079 .await?;
1080
1081 if !res.exit_status.success() {
1082 anyhow::bail!("Failed to mount VTL 2 pipette drive: {:?}", res);
1083 }
1084
1085 let res = self
1086 .openhcl_diag()?
1087 .run_detached_vtl2_command("sh", &["-c", "/cidata/pipette 2>&1 | logger &"])
1088 .await?;
1089
1090 if !res.success() {
1091 anyhow::bail!("Failed to spawn VTL 2 pipette: {:?}", res);
1092 }
1093
1094 Ok(())
1095 }
1096
1097 fn openhcl_diag(&self) -> anyhow::Result<&OpenHclDiagHandler> {
1098 if let Some(ohd) = self.openhcl_diag_handler.as_ref() {
1099 Ok(ohd)
1100 } else {
1101 anyhow::bail!("VM is not configured with OpenHCL")
1102 }
1103 }
1104
1105 pub async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
1107 self.runtime.get_guest_state_file().await
1108 }
1109}
1110
1111#[async_trait]
1113pub trait PetriVmRuntime: Send + Sync + 'static {
1114 type VmInspector: PetriVmInspector;
1116 type VmFramebufferAccess: PetriVmFramebufferAccess;
1118
1119 async fn teardown(self) -> anyhow::Result<()>;
1121 async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReason>;
1124 async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>;
1126 fn openhcl_diag(&self) -> Option<OpenHclDiagHandler>;
1128 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent>;
1131 async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()>;
1134 async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
1136 async fn restart_openhcl(
1139 &mut self,
1140 new_openhcl: &ResolvedArtifact,
1141 flags: OpenHclServicingFlags,
1142 ) -> anyhow::Result<()>;
1143 async fn save_openhcl(
1147 &mut self,
1148 new_openhcl: &ResolvedArtifact,
1149 flags: OpenHclServicingFlags,
1150 ) -> anyhow::Result<()>;
1151 async fn restore_openhcl(&mut self) -> anyhow::Result<()>;
1154 async fn update_command_line(&mut self, command_line: &str) -> anyhow::Result<()>;
1157 fn inspector(&self) -> Option<Self::VmInspector> {
1159 None
1160 }
1161 fn take_framebuffer_access(&mut self) -> Option<Self::VmFramebufferAccess> {
1164 None
1165 }
1166 async fn reset(&mut self) -> anyhow::Result<()>;
1168 async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
1170 Ok(None)
1171 }
1172}
1173
1174#[async_trait]
1176pub trait PetriVmInspector: Send + Sync + 'static {
1177 async fn inspect_all(&self) -> anyhow::Result<inspect::Node>;
1179}
1180
1181pub struct NoPetriVmInspector;
1183#[async_trait]
1184impl PetriVmInspector for NoPetriVmInspector {
1185 async fn inspect_all(&self) -> anyhow::Result<inspect::Node> {
1186 unreachable!()
1187 }
1188}
1189
1190pub struct VmScreenshotMeta {
1192 pub color: image::ExtendedColorType,
1194 pub width: u16,
1196 pub height: u16,
1198}
1199
1200#[async_trait]
1202pub trait PetriVmFramebufferAccess: Send + 'static {
1203 async fn screenshot(&mut self, image: &mut Vec<u8>)
1206 -> anyhow::Result<Option<VmScreenshotMeta>>;
1207}
1208
1209pub struct NoPetriVmFramebufferAccess;
1211#[async_trait]
1212impl PetriVmFramebufferAccess for NoPetriVmFramebufferAccess {
1213 async fn screenshot(
1214 &mut self,
1215 _image: &mut Vec<u8>,
1216 ) -> anyhow::Result<Option<VmScreenshotMeta>> {
1217 unreachable!()
1218 }
1219}
1220
1221#[derive(Debug)]
1223pub struct ProcessorTopology {
1224 pub vp_count: u32,
1226 pub enable_smt: Option<bool>,
1228 pub vps_per_socket: Option<u32>,
1230 pub apic_mode: Option<ApicMode>,
1232}
1233
1234impl Default for ProcessorTopology {
1235 fn default() -> Self {
1236 Self {
1237 vp_count: 2,
1238 enable_smt: None,
1239 vps_per_socket: None,
1240 apic_mode: None,
1241 }
1242 }
1243}
1244
1245#[derive(Debug, Clone, Copy)]
1247pub enum ApicMode {
1248 Xapic,
1250 X2apicSupported,
1252 X2apicEnabled,
1254}
1255
1256pub struct MemoryConfig {
1258 pub startup_bytes: u64,
1261 pub dynamic_memory_range: Option<(u64, u64)>,
1265}
1266
1267impl Default for MemoryConfig {
1268 fn default() -> Self {
1269 Self {
1270 startup_bytes: 0x1_0000_0000,
1271 dynamic_memory_range: None,
1272 }
1273 }
1274}
1275
1276#[derive(Debug)]
1278pub struct UefiConfig {
1279 pub secure_boot_enabled: bool,
1281 pub secure_boot_template: Option<SecureBootTemplate>,
1283 pub disable_frontpage: bool,
1285 pub default_boot_always_attempt: bool,
1287}
1288
1289impl Default for UefiConfig {
1290 fn default() -> Self {
1291 Self {
1292 secure_boot_enabled: false,
1293 secure_boot_template: None,
1294 disable_frontpage: true,
1295 default_boot_always_attempt: false,
1296 }
1297 }
1298}
1299
1300#[derive(Debug, Clone)]
1302pub enum OpenHclLogConfig {
1303 TestDefault,
1307 BuiltInDefault,
1310 Custom(String),
1313}
1314
1315#[derive(Debug, Clone)]
1317pub struct OpenHclConfig {
1318 pub vtl2_nvme_boot: bool,
1321 pub vmbus_redirect: bool,
1323 pub command_line: Option<String>,
1327 pub log_levels: OpenHclLogConfig,
1331 pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
1334}
1335
1336impl OpenHclConfig {
1337 pub fn command_line(&self) -> String {
1340 let mut cmdline = self.command_line.clone();
1341
1342 append_cmdline(&mut cmdline, "OPENHCL_MANA_KEEP_ALIVE=host,privatepool");
1344
1345 match &self.log_levels {
1346 OpenHclLogConfig::TestDefault => {
1347 let default_log_levels = {
1348 let openhcl_tracing = if let Ok(x) =
1350 std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG"))
1351 {
1352 format!("OPENVMM_LOG={x}")
1353 } else {
1354 "OPENVMM_LOG=debug".to_owned()
1355 };
1356 let openhcl_show_spans = if let Ok(x) = std::env::var("OPENVMM_SHOW_SPANS") {
1357 format!("OPENVMM_SHOW_SPANS={x}")
1358 } else {
1359 "OPENVMM_SHOW_SPANS=true".to_owned()
1360 };
1361 format!("{openhcl_tracing} {openhcl_show_spans}")
1362 };
1363 append_cmdline(&mut cmdline, &default_log_levels);
1364 }
1365 OpenHclLogConfig::BuiltInDefault => {
1366 }
1368 OpenHclLogConfig::Custom(levels) => {
1369 append_cmdline(&mut cmdline, levels);
1370 }
1371 }
1372
1373 cmdline.unwrap_or_default()
1374 }
1375}
1376
1377impl Default for OpenHclConfig {
1378 fn default() -> Self {
1379 Self {
1380 vtl2_nvme_boot: false,
1381 vmbus_redirect: false,
1382 command_line: None,
1383 log_levels: OpenHclLogConfig::TestDefault,
1384 vtl2_base_address_type: None,
1385 }
1386 }
1387}
1388
1389#[derive(Debug)]
1391pub struct TpmConfig {
1392 pub no_persistent_secrets: bool,
1394}
1395
1396impl Default for TpmConfig {
1397 fn default() -> Self {
1398 Self {
1399 no_persistent_secrets: true,
1400 }
1401 }
1402}
1403
1404#[derive(Debug)]
1406pub enum Firmware {
1407 LinuxDirect {
1409 kernel: ResolvedArtifact,
1411 initrd: ResolvedArtifact,
1413 },
1414 OpenhclLinuxDirect {
1416 igvm_path: ResolvedArtifact,
1418 openhcl_config: OpenHclConfig,
1420 },
1421 Pcat {
1423 guest: PcatGuest,
1425 bios_firmware: ResolvedOptionalArtifact,
1427 svga_firmware: ResolvedOptionalArtifact,
1429 },
1430 OpenhclPcat {
1432 guest: PcatGuest,
1434 igvm_path: ResolvedArtifact,
1436 bios_firmware: ResolvedOptionalArtifact,
1438 svga_firmware: ResolvedOptionalArtifact,
1440 openhcl_config: OpenHclConfig,
1442 },
1443 Uefi {
1445 guest: UefiGuest,
1447 uefi_firmware: ResolvedArtifact,
1449 uefi_config: UefiConfig,
1451 },
1452 OpenhclUefi {
1454 guest: UefiGuest,
1456 isolation: Option<IsolationType>,
1458 igvm_path: ResolvedArtifact,
1460 uefi_config: UefiConfig,
1462 openhcl_config: OpenHclConfig,
1464 },
1465}
1466
1467#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1469pub enum BootDeviceType {
1470 None,
1472 Ide,
1474 Scsi,
1476 Nvme,
1478}
1479
1480impl Firmware {
1481 pub fn linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
1483 use petri_artifacts_vmm_test::artifacts::loadable::*;
1484 match arch {
1485 MachineArch::X86_64 => Firmware::LinuxDirect {
1486 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_X64).erase(),
1487 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
1488 },
1489 MachineArch::Aarch64 => Firmware::LinuxDirect {
1490 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_AARCH64).erase(),
1491 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_AARCH64).erase(),
1492 },
1493 }
1494 }
1495
1496 pub fn openhcl_linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
1498 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
1499 match arch {
1500 MachineArch::X86_64 => Firmware::OpenhclLinuxDirect {
1501 igvm_path: resolver.require(LATEST_LINUX_DIRECT_TEST_X64).erase(),
1502 openhcl_config: Default::default(),
1503 },
1504 MachineArch::Aarch64 => todo!("Linux direct not yet supported on aarch64"),
1505 }
1506 }
1507
1508 pub fn pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
1510 use petri_artifacts_vmm_test::artifacts::loadable::*;
1511 Firmware::Pcat {
1512 guest,
1513 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
1514 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
1515 }
1516 }
1517
1518 pub fn uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch, guest: UefiGuest) -> Self {
1520 use petri_artifacts_vmm_test::artifacts::loadable::*;
1521 let uefi_firmware = match arch {
1522 MachineArch::X86_64 => resolver.require(UEFI_FIRMWARE_X64).erase(),
1523 MachineArch::Aarch64 => resolver.require(UEFI_FIRMWARE_AARCH64).erase(),
1524 };
1525 Firmware::Uefi {
1526 guest,
1527 uefi_firmware,
1528 uefi_config: Default::default(),
1529 }
1530 }
1531
1532 pub fn openhcl_uefi(
1534 resolver: &ArtifactResolver<'_>,
1535 arch: MachineArch,
1536 guest: UefiGuest,
1537 isolation: Option<IsolationType>,
1538 vtl2_nvme_boot: bool,
1539 ) -> Self {
1540 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
1541 let igvm_path = match arch {
1542 MachineArch::X86_64 if isolation.is_some() => resolver.require(LATEST_CVM_X64).erase(),
1543 MachineArch::X86_64 => resolver.require(LATEST_STANDARD_X64).erase(),
1544 MachineArch::Aarch64 => resolver.require(LATEST_STANDARD_AARCH64).erase(),
1545 };
1546 Firmware::OpenhclUefi {
1547 guest,
1548 isolation,
1549 igvm_path,
1550 uefi_config: Default::default(),
1551 openhcl_config: OpenHclConfig {
1552 vtl2_nvme_boot,
1553 ..Default::default()
1554 },
1555 }
1556 }
1557
1558 fn is_openhcl(&self) -> bool {
1559 match self {
1560 Firmware::OpenhclLinuxDirect { .. }
1561 | Firmware::OpenhclUefi { .. }
1562 | Firmware::OpenhclPcat { .. } => true,
1563 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => false,
1564 }
1565 }
1566
1567 fn isolation(&self) -> Option<IsolationType> {
1568 match self {
1569 Firmware::OpenhclUefi { isolation, .. } => *isolation,
1570 Firmware::LinuxDirect { .. }
1571 | Firmware::Pcat { .. }
1572 | Firmware::Uefi { .. }
1573 | Firmware::OpenhclLinuxDirect { .. }
1574 | Firmware::OpenhclPcat { .. } => None,
1575 }
1576 }
1577
1578 fn is_linux_direct(&self) -> bool {
1579 match self {
1580 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => true,
1581 Firmware::Pcat { .. }
1582 | Firmware::Uefi { .. }
1583 | Firmware::OpenhclUefi { .. }
1584 | Firmware::OpenhclPcat { .. } => false,
1585 }
1586 }
1587
1588 fn is_pcat(&self) -> bool {
1589 match self {
1590 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => true,
1591 Firmware::Uefi { .. }
1592 | Firmware::OpenhclUefi { .. }
1593 | Firmware::LinuxDirect { .. }
1594 | Firmware::OpenhclLinuxDirect { .. } => false,
1595 }
1596 }
1597
1598 fn os_flavor(&self) -> OsFlavor {
1599 match self {
1600 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => OsFlavor::Linux,
1601 Firmware::Uefi {
1602 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
1603 ..
1604 }
1605 | Firmware::OpenhclUefi {
1606 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
1607 ..
1608 } => OsFlavor::Uefi,
1609 Firmware::Pcat {
1610 guest: PcatGuest::Vhd(cfg),
1611 ..
1612 }
1613 | Firmware::OpenhclPcat {
1614 guest: PcatGuest::Vhd(cfg),
1615 ..
1616 }
1617 | Firmware::Uefi {
1618 guest: UefiGuest::Vhd(cfg),
1619 ..
1620 }
1621 | Firmware::OpenhclUefi {
1622 guest: UefiGuest::Vhd(cfg),
1623 ..
1624 } => cfg.os_flavor,
1625 Firmware::Pcat {
1626 guest: PcatGuest::Iso(cfg),
1627 ..
1628 }
1629 | Firmware::OpenhclPcat {
1630 guest: PcatGuest::Iso(cfg),
1631 ..
1632 } => cfg.os_flavor,
1633 }
1634 }
1635
1636 fn quirks(&self) -> GuestQuirks {
1637 match self {
1638 Firmware::Pcat {
1639 guest: PcatGuest::Vhd(cfg),
1640 ..
1641 }
1642 | Firmware::Uefi {
1643 guest: UefiGuest::Vhd(cfg),
1644 ..
1645 }
1646 | Firmware::OpenhclUefi {
1647 guest: UefiGuest::Vhd(cfg),
1648 ..
1649 } => cfg.quirks.clone(),
1650 Firmware::Pcat {
1651 guest: PcatGuest::Iso(cfg),
1652 ..
1653 } => cfg.quirks.clone(),
1654 _ => Default::default(),
1655 }
1656 }
1657
1658 fn expected_boot_event(&self) -> Option<FirmwareEvent> {
1659 match self {
1660 Firmware::LinuxDirect { .. }
1661 | Firmware::OpenhclLinuxDirect { .. }
1662 | Firmware::Uefi {
1663 guest: UefiGuest::GuestTestUefi(_),
1664 ..
1665 }
1666 | Firmware::OpenhclUefi {
1667 guest: UefiGuest::GuestTestUefi(_),
1668 ..
1669 } => None,
1670 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => {
1671 Some(FirmwareEvent::BootAttempt)
1673 }
1674 Firmware::Uefi {
1675 guest: UefiGuest::None,
1676 ..
1677 }
1678 | Firmware::OpenhclUefi {
1679 guest: UefiGuest::None,
1680 ..
1681 } => Some(FirmwareEvent::NoBootDevice),
1682 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => {
1683 Some(FirmwareEvent::BootSuccess)
1684 }
1685 }
1686 }
1687
1688 fn openhcl_config(&self) -> Option<&OpenHclConfig> {
1689 match self {
1690 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
1691 | Firmware::OpenhclUefi { openhcl_config, .. }
1692 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
1693 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
1694 }
1695 }
1696
1697 fn openhcl_config_mut(&mut self) -> Option<&mut OpenHclConfig> {
1698 match self {
1699 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
1700 | Firmware::OpenhclUefi { openhcl_config, .. }
1701 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
1702 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
1703 }
1704 }
1705
1706 fn uefi_config(&self) -> Option<&UefiConfig> {
1707 match self {
1708 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
1709 Some(uefi_config)
1710 }
1711 Firmware::LinuxDirect { .. }
1712 | Firmware::OpenhclLinuxDirect { .. }
1713 | Firmware::Pcat { .. }
1714 | Firmware::OpenhclPcat { .. } => None,
1715 }
1716 }
1717
1718 fn uefi_config_mut(&mut self) -> Option<&mut UefiConfig> {
1719 match self {
1720 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
1721 Some(uefi_config)
1722 }
1723 Firmware::LinuxDirect { .. }
1724 | Firmware::OpenhclLinuxDirect { .. }
1725 | Firmware::Pcat { .. }
1726 | Firmware::OpenhclPcat { .. } => None,
1727 }
1728 }
1729}
1730
1731#[derive(Debug)]
1734pub enum PcatGuest {
1735 Vhd(BootImageConfig<boot_image_type::Vhd>),
1737 Iso(BootImageConfig<boot_image_type::Iso>),
1739}
1740
1741impl PcatGuest {
1742 fn artifact(&self) -> &ResolvedArtifact {
1743 match self {
1744 PcatGuest::Vhd(disk) => &disk.artifact,
1745 PcatGuest::Iso(disk) => &disk.artifact,
1746 }
1747 }
1748}
1749
1750#[derive(Debug)]
1753pub enum UefiGuest {
1754 Vhd(BootImageConfig<boot_image_type::Vhd>),
1756 GuestTestUefi(ResolvedArtifact),
1758 None,
1760}
1761
1762impl UefiGuest {
1763 pub fn guest_test_uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
1765 use petri_artifacts_vmm_test::artifacts::test_vhd::*;
1766 let artifact = match arch {
1767 MachineArch::X86_64 => resolver.require(GUEST_TEST_UEFI_X64).erase(),
1768 MachineArch::Aarch64 => resolver.require(GUEST_TEST_UEFI_AARCH64).erase(),
1769 };
1770 UefiGuest::GuestTestUefi(artifact)
1771 }
1772
1773 fn artifact(&self) -> Option<&ResolvedArtifact> {
1774 match self {
1775 UefiGuest::Vhd(vhd) => Some(&vhd.artifact),
1776 UefiGuest::GuestTestUefi(p) => Some(p),
1777 UefiGuest::None => None,
1778 }
1779 }
1780}
1781
1782pub mod boot_image_type {
1784 mod private {
1785 pub trait Sealed {}
1786 impl Sealed for super::Vhd {}
1787 impl Sealed for super::Iso {}
1788 }
1789
1790 pub trait BootImageType: private::Sealed {}
1793
1794 #[derive(Debug)]
1796 pub enum Vhd {}
1797
1798 #[derive(Debug)]
1800 pub enum Iso {}
1801
1802 impl BootImageType for Vhd {}
1803 impl BootImageType for Iso {}
1804}
1805
1806#[derive(Debug)]
1808pub struct BootImageConfig<T: boot_image_type::BootImageType> {
1809 artifact: ResolvedArtifact,
1811 os_flavor: OsFlavor,
1813 quirks: GuestQuirks,
1817 _type: core::marker::PhantomData<T>,
1819}
1820
1821impl BootImageConfig<boot_image_type::Vhd> {
1822 pub fn from_vhd<A>(artifact: ResolvedArtifact<A>) -> Self
1824 where
1825 A: petri_artifacts_common::tags::IsTestVhd,
1826 {
1827 BootImageConfig {
1828 artifact: artifact.erase(),
1829 os_flavor: A::OS_FLAVOR,
1830 quirks: A::quirks(),
1831 _type: std::marker::PhantomData,
1832 }
1833 }
1834}
1835
1836impl BootImageConfig<boot_image_type::Iso> {
1837 pub fn from_iso<A>(artifact: ResolvedArtifact<A>) -> Self
1839 where
1840 A: petri_artifacts_common::tags::IsTestIso,
1841 {
1842 BootImageConfig {
1843 artifact: artifact.erase(),
1844 os_flavor: A::OS_FLAVOR,
1845 quirks: A::quirks(),
1846 _type: std::marker::PhantomData,
1847 }
1848 }
1849}
1850
1851#[derive(Debug, Clone, Copy)]
1853pub enum IsolationType {
1854 Vbs,
1856 Snp,
1858 Tdx,
1860}
1861
1862#[derive(Debug, Clone, Copy)]
1864pub struct OpenHclServicingFlags {
1865 pub enable_nvme_keepalive: bool,
1868 pub enable_mana_keepalive: bool,
1870 pub override_version_checks: bool,
1872 pub stop_timeout_hint_secs: Option<u16>,
1874}
1875
1876#[derive(Debug, Clone)]
1878pub enum PetriDiskType {
1879 Memory,
1881 Differencing(PathBuf),
1883 Persistent(PathBuf),
1885}
1886
1887#[derive(Debug, Clone)]
1889pub struct PetriVmgsDisk {
1890 pub disk: PetriDiskType,
1892 pub encryption_policy: GuestStateEncryptionPolicy,
1894}
1895
1896impl Default for PetriVmgsDisk {
1897 fn default() -> Self {
1898 PetriVmgsDisk {
1899 disk: PetriDiskType::Memory,
1900 encryption_policy: GuestStateEncryptionPolicy::None(false),
1902 }
1903 }
1904}
1905
1906#[derive(Debug, Clone)]
1908pub enum PetriVmgsResource {
1909 Disk(PetriVmgsDisk),
1911 ReprovisionOnFailure(PetriVmgsDisk),
1913 Reprovision(PetriVmgsDisk),
1915 Ephemeral,
1917}
1918
1919impl PetriVmgsResource {
1920 pub fn disk(&self) -> Option<&PetriVmgsDisk> {
1922 match self {
1923 PetriVmgsResource::Disk(vmgs)
1924 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1925 | PetriVmgsResource::Reprovision(vmgs) => Some(vmgs),
1926 PetriVmgsResource::Ephemeral => None,
1927 }
1928 }
1929}
1930
1931#[derive(Debug, Clone, Copy)]
1933pub enum PetriGuestStateLifetime {
1934 Disk,
1937 ReprovisionOnFailure,
1939 Reprovision,
1941 Ephemeral,
1943}
1944
1945#[derive(Debug, Clone, Copy)]
1947pub enum SecureBootTemplate {
1948 MicrosoftWindows,
1950 MicrosoftUefiCertificateAuthority,
1952}
1953
1954#[derive(Default, Debug, Clone)]
1957pub struct VmmQuirks {
1958 pub flaky_boot: Option<Duration>,
1961}
1962
1963fn make_vm_safe_name(name: &str) -> String {
1969 const MAX_VM_NAME_LENGTH: usize = 100;
1970 const HASH_LENGTH: usize = 4;
1971 const MAX_PREFIX_LENGTH: usize = MAX_VM_NAME_LENGTH - HASH_LENGTH;
1972
1973 if name.len() <= MAX_VM_NAME_LENGTH {
1974 name.to_owned()
1975 } else {
1976 let mut hasher = DefaultHasher::new();
1978 name.hash(&mut hasher);
1979 let hash = hasher.finish();
1980
1981 let hash_suffix = format!("{:04x}", hash & 0xFFFF);
1983
1984 let truncated = &name[..MAX_PREFIX_LENGTH];
1986 tracing::debug!(
1987 "VM name too long ({}), truncating '{}' to '{}{}'",
1988 name.len(),
1989 name,
1990 truncated,
1991 hash_suffix
1992 );
1993
1994 format!("{}{}", truncated, hash_suffix)
1995 }
1996}
1997
1998#[derive(Debug, Clone, Copy, Eq, PartialEq)]
2000pub enum PetriHaltReason {
2001 PowerOff,
2003 Reset,
2005 Hibernate,
2007 TripleFault,
2009 Other,
2011}
2012
2013fn append_cmdline(cmd: &mut Option<String>, add_cmd: impl AsRef<str>) {
2014 if let Some(cmd) = cmd.as_mut() {
2015 cmd.push(' ');
2016 cmd.push_str(add_cmd.as_ref());
2017 } else {
2018 *cmd = Some(add_cmd.as_ref().to_string());
2019 }
2020}
2021
2022async fn save_inspect(
2023 name: &str,
2024 inspect: std::pin::Pin<Box<dyn Future<Output = anyhow::Result<inspect::Node>> + Send>>,
2025 log_source: &PetriLogSource,
2026) {
2027 tracing::info!("Collecting {name} inspect details.");
2028 let node = match inspect.await {
2029 Ok(n) => n,
2030 Err(e) => {
2031 tracing::error!(?e, "Failed to get {name}");
2032 return;
2033 }
2034 };
2035 if let Err(e) = log_source.write_attachment(
2036 &format!("timeout_inspect_{name}.log"),
2037 format!("{node:#}").as_bytes(),
2038 ) {
2039 tracing::error!(?e, "Failed to save {name} inspect log");
2040 return;
2041 }
2042 tracing::info!("{name} inspect task finished.");
2043}
2044
2045#[cfg(test)]
2046mod tests {
2047 use super::make_vm_safe_name;
2048
2049 #[test]
2050 fn test_short_names_unchanged() {
2051 let short_name = "short_test_name";
2052 assert_eq!(make_vm_safe_name(short_name), short_name);
2053 }
2054
2055 #[test]
2056 fn test_exactly_100_chars_unchanged() {
2057 let name_100 = "a".repeat(100);
2058 assert_eq!(make_vm_safe_name(&name_100), name_100);
2059 }
2060
2061 #[test]
2062 fn test_long_name_truncated() {
2063 let long_name = "multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_openhcl_servicing";
2064 let result = make_vm_safe_name(long_name);
2065
2066 assert_eq!(result.len(), 100);
2068
2069 assert!(result.starts_with("multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_ope"));
2071
2072 let suffix = &result[96..];
2074 assert_eq!(suffix.len(), 4);
2075 assert!(u16::from_str_radix(suffix, 16).is_ok());
2077 }
2078
2079 #[test]
2080 fn test_deterministic_results() {
2081 let long_name = "very_long_test_name_that_exceeds_the_100_character_limit_and_should_be_truncated_consistently_every_time";
2082 let result1 = make_vm_safe_name(long_name);
2083 let result2 = make_vm_safe_name(long_name);
2084
2085 assert_eq!(result1, result2);
2086 assert_eq!(result1.len(), 100);
2087 }
2088
2089 #[test]
2090 fn test_different_names_different_hashes() {
2091 let name1 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_1";
2092 let name2 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_2";
2093
2094 let result1 = make_vm_safe_name(name1);
2095 let result2 = make_vm_safe_name(name2);
2096
2097 assert_eq!(result1.len(), 100);
2099 assert_eq!(result2.len(), 100);
2100
2101 assert_ne!(result1, result2);
2103 assert_ne!(&result1[96..], &result2[96..]);
2104 }
2105}