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::openhcl_diag::OpenHclDiagHandler;
16use async_trait::async_trait;
17use get_resources::ged::FirmwareEvent;
18use hvlite_defs::config::Vtl2BaseAddressType;
19use mesh::CancelContext;
20use pal_async::DefaultDriver;
21use pal_async::task::Spawn;
22use pal_async::task::Task;
23use pal_async::timer::PolledTimer;
24use petri_artifacts_common::tags::GuestQuirks;
25use petri_artifacts_common::tags::GuestQuirksInner;
26use petri_artifacts_common::tags::InitialRebootCondition;
27use petri_artifacts_common::tags::IsOpenhclIgvm;
28use petri_artifacts_common::tags::IsTestVmgs;
29use petri_artifacts_common::tags::MachineArch;
30use petri_artifacts_common::tags::OsFlavor;
31use petri_artifacts_core::ArtifactResolver;
32use petri_artifacts_core::ResolvedArtifact;
33use petri_artifacts_core::ResolvedOptionalArtifact;
34use pipette_client::PipetteClient;
35use std::collections::hash_map::DefaultHasher;
36use std::hash::Hash;
37use std::hash::Hasher;
38use std::path::Path;
39use std::path::PathBuf;
40use std::time::Duration;
41use vmgs_resources::GuestStateEncryptionPolicy;
42
43pub struct PetriVmArtifacts<T: PetriVmmBackend> {
46 pub backend: T,
48 pub firmware: Firmware,
50 pub arch: MachineArch,
52 pub agent_image: Option<AgentImage>,
54 pub openhcl_agent_image: Option<AgentImage>,
56}
57
58impl<T: PetriVmmBackend> PetriVmArtifacts<T> {
59 pub fn new(
63 resolver: &ArtifactResolver<'_>,
64 firmware: Firmware,
65 arch: MachineArch,
66 with_vtl0_pipette: bool,
67 ) -> Option<Self> {
68 if !T::check_compat(&firmware, arch) {
69 return None;
70 }
71
72 Some(Self {
73 backend: T::new(resolver),
74 arch,
75 agent_image: Some(if with_vtl0_pipette {
76 AgentImage::new(firmware.os_flavor()).with_pipette(resolver, arch)
77 } else {
78 AgentImage::new(firmware.os_flavor())
79 }),
80 openhcl_agent_image: if firmware.is_openhcl() {
81 Some(AgentImage::new(OsFlavor::Linux).with_pipette(resolver, arch))
82 } else {
83 None
84 },
85 firmware,
86 })
87 }
88}
89
90pub struct PetriVmBuilder<T: PetriVmmBackend> {
92 backend: T,
94 config: PetriVmConfig,
96 modify_vmm_config: Option<Box<dyn FnOnce(T::VmmConfig) -> T::VmmConfig + Send>>,
98 resources: PetriVmResources,
100
101 guest_quirks: GuestQuirksInner,
103 vmm_quirks: VmmQuirks,
104
105 expected_boot_event: Option<FirmwareEvent>,
108 override_expect_reset: bool,
109}
110
111pub struct PetriVmConfig {
113 pub name: String,
115 pub arch: MachineArch,
117 pub firmware: Firmware,
119 pub memory: MemoryConfig,
121 pub proc_topology: ProcessorTopology,
123 pub agent_image: Option<AgentImage>,
125 pub openhcl_agent_image: Option<AgentImage>,
127 pub vmgs: PetriVmgsResource,
129 pub boot_device_type: BootDeviceType,
131 pub tpm_state_persistence: bool,
133}
134
135pub struct PetriVmResources {
137 driver: DefaultDriver,
138 log_source: PetriLogSource,
139}
140
141#[async_trait]
143pub trait PetriVmmBackend {
144 type VmmConfig;
146
147 type VmRuntime: PetriVmRuntime;
149
150 fn check_compat(firmware: &Firmware, arch: MachineArch) -> bool;
153
154 fn quirks(firmware: &Firmware) -> (GuestQuirksInner, VmmQuirks);
156
157 fn new(resolver: &ArtifactResolver<'_>) -> Self;
159
160 async fn run(
162 self,
163 config: PetriVmConfig,
164 modify_vmm_config: Option<impl FnOnce(Self::VmmConfig) -> Self::VmmConfig + Send>,
165 resources: &PetriVmResources,
166 ) -> anyhow::Result<Self::VmRuntime>;
167}
168
169pub struct PetriVm<T: PetriVmmBackend> {
171 resources: PetriVmResources,
172 runtime: T::VmRuntime,
173 watchdog_tasks: Vec<Task<()>>,
174 openhcl_diag_handler: Option<OpenHclDiagHandler>,
175
176 arch: MachineArch,
177 guest_quirks: GuestQuirksInner,
178 vmm_quirks: VmmQuirks,
179 expected_boot_event: Option<FirmwareEvent>,
180}
181
182impl<T: PetriVmmBackend> PetriVmBuilder<T> {
183 pub fn new(
185 params: &PetriTestParams<'_>,
186 artifacts: PetriVmArtifacts<T>,
187 driver: &DefaultDriver,
188 ) -> anyhow::Result<Self> {
189 let (guest_quirks, vmm_quirks) = T::quirks(&artifacts.firmware);
190 let expected_boot_event = artifacts.firmware.expected_boot_event();
191 let boot_device_type = match artifacts.firmware {
192 Firmware::LinuxDirect { .. } => BootDeviceType::None,
193 Firmware::OpenhclLinuxDirect { .. } => BootDeviceType::None,
194 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => BootDeviceType::Ide,
195 Firmware::Uefi {
196 guest: UefiGuest::None,
197 ..
198 }
199 | Firmware::OpenhclUefi {
200 guest: UefiGuest::None,
201 ..
202 } => BootDeviceType::None,
203 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => BootDeviceType::Scsi,
204 };
205
206 Ok(Self {
207 backend: artifacts.backend,
208 config: PetriVmConfig {
209 name: make_vm_safe_name(params.test_name),
210 arch: artifacts.arch,
211 firmware: artifacts.firmware,
212 boot_device_type,
213 memory: Default::default(),
214 proc_topology: Default::default(),
215 agent_image: artifacts.agent_image,
216 openhcl_agent_image: artifacts.openhcl_agent_image,
217 vmgs: PetriVmgsResource::Ephemeral,
218 tpm_state_persistence: true,
219 },
220 modify_vmm_config: None,
221 resources: PetriVmResources {
222 driver: driver.clone(),
223 log_source: params.logger.clone(),
224 },
225
226 guest_quirks,
227 vmm_quirks,
228 expected_boot_event,
229 override_expect_reset: false,
230 })
231 }
232}
233
234impl<T: PetriVmmBackend> PetriVmBuilder<T> {
235 pub async fn run_without_agent(self) -> anyhow::Result<PetriVm<T>> {
239 self.run_core().await
240 }
241
242 pub async fn run(self) -> anyhow::Result<(PetriVm<T>, PipetteClient)> {
245 assert!(self.config.agent_image.is_some());
246 assert!(self.config.agent_image.as_ref().unwrap().contains_pipette());
247
248 let mut vm = self.run_core().await?;
249 let client = vm.wait_for_agent().await?;
250 Ok((vm, client))
251 }
252
253 async fn run_core(self) -> anyhow::Result<PetriVm<T>> {
254 let arch = self.config.arch;
255 let expect_reset = self.expect_reset();
256
257 let mut runtime = self
258 .backend
259 .run(self.config, self.modify_vmm_config, &self.resources)
260 .await?;
261 let openhcl_diag_handler = runtime.openhcl_diag();
262 let watchdog_tasks = Self::start_watchdog_tasks(&self.resources, &mut runtime)?;
263
264 let mut vm = PetriVm {
265 resources: self.resources,
266 runtime,
267 watchdog_tasks,
268 openhcl_diag_handler,
269
270 arch,
271 guest_quirks: self.guest_quirks,
272 vmm_quirks: self.vmm_quirks,
273 expected_boot_event: self.expected_boot_event,
274 };
275
276 if expect_reset {
277 vm.wait_for_reset_core().await?;
278 }
279
280 vm.wait_for_expected_boot_event().await?;
281
282 Ok(vm)
283 }
284
285 fn expect_reset(&self) -> bool {
286 self.override_expect_reset
288 || matches!(
289 (
290 self.guest_quirks.initial_reboot,
291 self.expected_boot_event,
292 &self.config.firmware,
293 ),
294 (
295 Some(InitialRebootCondition::Always),
296 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
297 _,
298 ) | (
299 Some(InitialRebootCondition::WithOpenHclUefi),
300 Some(FirmwareEvent::BootSuccess | FirmwareEvent::BootAttempt),
301 Firmware::OpenhclUefi { .. },
302 )
303 )
304 }
305
306 fn start_watchdog_tasks(
307 resources: &PetriVmResources,
308 runtime: &mut T::VmRuntime,
309 ) -> anyhow::Result<Vec<Task<()>>> {
310 let mut tasks = Vec::new();
311
312 {
313 const TIMEOUT_DURATION_MINUTES: u64 = 10;
314 const TIMER_DURATION: Duration = Duration::from_secs(TIMEOUT_DURATION_MINUTES * 60);
315 let log_source = resources.log_source.clone();
316 let inspect_task =
317 |name,
318 driver: &DefaultDriver,
319 inspect: std::pin::Pin<Box<dyn Future<Output = _> + Send>>| {
320 driver.spawn(format!("petri-watchdog-inspect-{name}"), async move {
321 save_inspect(name, inspect, &log_source).await;
322 })
323 };
324
325 let driver = resources.driver.clone();
326 let vmm_inspector = runtime.inspector();
327 let openhcl_diag_handler = runtime.openhcl_diag();
328 tasks.push(resources.driver.spawn("timer-watchdog", async move {
329 PolledTimer::new(&driver).sleep(TIMER_DURATION).await;
330 tracing::warn!("Test timeout reached after {TIMEOUT_DURATION_MINUTES} minutes, collecting diagnostics.");
331 let mut timeout_tasks = Vec::new();
332 if let Some(inspector) = vmm_inspector {
333 timeout_tasks.push(inspect_task.clone()("vmm", &driver, Box::pin(async move { inspector.inspect_all().await })) );
334 }
335 if let Some(openhcl_diag_handler) = openhcl_diag_handler {
336 timeout_tasks.push(inspect_task("openhcl", &driver, Box::pin(async move { openhcl_diag_handler.inspect("", None, None).await })));
337 }
338 futures::future::join_all(timeout_tasks).await;
339 tracing::error!("Test time out diagnostics collection complete, aborting.");
340 panic!("Test timed out");
341 }));
342 }
343
344 if let Some(mut framebuffer_access) = runtime.take_framebuffer_access() {
345 let mut timer = PolledTimer::new(&resources.driver);
346 let log_source = resources.log_source.clone();
347
348 tasks.push(
349 resources
350 .driver
351 .spawn("petri-watchdog-screenshot", async move {
352 let mut image = Vec::new();
353 let mut last_image = Vec::new();
354 loop {
355 timer.sleep(Duration::from_secs(2)).await;
356 tracing::trace!("Taking screenshot.");
357
358 let VmScreenshotMeta {
359 color,
360 width,
361 height,
362 } = match framebuffer_access.screenshot(&mut image).await {
363 Ok(Some(meta)) => meta,
364 Ok(None) => {
365 tracing::debug!("VM off, skipping screenshot.");
366 continue;
367 }
368 Err(e) => {
369 tracing::error!(?e, "Failed to take screenshot");
370 continue;
371 }
372 };
373
374 if image == last_image {
375 tracing::debug!("No change in framebuffer, skipping screenshot.");
376 continue;
377 }
378
379 let r =
380 log_source
381 .create_attachment("screenshot.png")
382 .and_then(|mut f| {
383 image::write_buffer_with_format(
384 &mut f,
385 &image,
386 width.into(),
387 height.into(),
388 color,
389 image::ImageFormat::Png,
390 )
391 .map_err(Into::into)
392 });
393
394 if let Err(e) = r {
395 tracing::error!(?e, "Failed to save screenshot");
396 } else {
397 tracing::info!("Screenshot saved.");
398 }
399
400 std::mem::swap(&mut image, &mut last_image);
401 }
402 }),
403 );
404 }
405
406 Ok(tasks)
407 }
408
409 pub fn with_expect_boot_failure(mut self) -> Self {
412 self.expected_boot_event = Some(FirmwareEvent::BootFailed);
413 self
414 }
415
416 pub fn with_expect_no_boot_event(mut self) -> Self {
419 self.expected_boot_event = None;
420 self
421 }
422
423 pub fn with_expect_reset(mut self) -> Self {
427 self.override_expect_reset = true;
428 self
429 }
430
431 pub fn with_secure_boot(mut self) -> Self {
433 self.config
434 .firmware
435 .uefi_config_mut()
436 .expect("Secure boot is only supported for UEFI firmware.")
437 .secure_boot_enabled = true;
438
439 match self.os_flavor() {
440 OsFlavor::Windows => self.with_windows_secure_boot_template(),
441 OsFlavor::Linux => self.with_uefi_ca_secure_boot_template(),
442 _ => panic!(
443 "Secure boot unsupported for OS flavor {:?}",
444 self.os_flavor()
445 ),
446 }
447 }
448
449 pub fn with_windows_secure_boot_template(mut self) -> Self {
451 self.config
452 .firmware
453 .uefi_config_mut()
454 .expect("Secure boot is only supported for UEFI firmware.")
455 .secure_boot_template = Some(SecureBootTemplate::MicrosoftWindows);
456 self
457 }
458
459 pub fn with_uefi_ca_secure_boot_template(mut self) -> Self {
461 self.config
462 .firmware
463 .uefi_config_mut()
464 .expect("Secure boot is only supported for UEFI firmware.")
465 .secure_boot_template = Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority);
466 self
467 }
468
469 pub fn with_processor_topology(mut self, topology: ProcessorTopology) -> Self {
471 self.config.proc_topology = topology;
472 self
473 }
474
475 pub fn with_memory(mut self, memory: MemoryConfig) -> Self {
477 self.config.memory = memory;
478 self
479 }
480
481 pub fn with_vtl2_base_address_type(mut self, address_type: Vtl2BaseAddressType) -> Self {
486 self.config
487 .firmware
488 .openhcl_config_mut()
489 .expect("OpenHCL firmware is required to set custom VTL2 address type.")
490 .vtl2_base_address_type = Some(address_type);
491 self
492 }
493
494 pub fn with_custom_openhcl(mut self, artifact: ResolvedArtifact<impl IsOpenhclIgvm>) -> Self {
496 match &mut self.config.firmware {
497 Firmware::OpenhclLinuxDirect { igvm_path, .. }
498 | Firmware::OpenhclPcat { igvm_path, .. }
499 | Firmware::OpenhclUefi { igvm_path, .. } => {
500 *igvm_path = artifact.erase();
501 }
502 Firmware::LinuxDirect { .. } | Firmware::Uefi { .. } | Firmware::Pcat { .. } => {
503 panic!("Custom OpenHCL is only supported for OpenHCL firmware.")
504 }
505 }
506 self
507 }
508
509 pub fn with_openhcl_command_line(mut self, additional_command_line: &str) -> Self {
511 append_cmdline(
512 &mut self
513 .config
514 .firmware
515 .openhcl_config_mut()
516 .expect("OpenHCL command line is only supported for OpenHCL firmware.")
517 .command_line,
518 additional_command_line,
519 );
520 self
521 }
522
523 pub fn with_confidential_filtering(self) -> Self {
525 if !self.config.firmware.is_openhcl() {
526 panic!("Confidential filtering is only supported for OpenHCL");
527 }
528 self.with_openhcl_command_line(&format!(
529 "{}=1 {}=0",
530 underhill_confidentiality::OPENHCL_CONFIDENTIAL_ENV_VAR_NAME,
531 underhill_confidentiality::OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME
532 ))
533 }
534
535 pub fn with_openhcl_log_levels(mut self, levels: OpenHclLogConfig) -> Self {
537 self.config
538 .firmware
539 .openhcl_config_mut()
540 .expect("OpenHCL firmware is required to set custom OpenHCL log levels.")
541 .log_levels = levels;
542 self
543 }
544
545 pub fn with_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
547 self.config
548 .agent_image
549 .as_mut()
550 .expect("no guest pipette")
551 .add_file(name, artifact);
552 self
553 }
554
555 pub fn with_openhcl_agent_file(mut self, name: &str, artifact: ResolvedArtifact) -> Self {
557 self.config
558 .openhcl_agent_image
559 .as_mut()
560 .expect("no openhcl pipette")
561 .add_file(name, artifact);
562 self
563 }
564
565 pub fn with_uefi_frontpage(mut self, enable: bool) -> Self {
567 self.config
568 .firmware
569 .uefi_config_mut()
570 .expect("UEFI frontpage is only supported for UEFI firmware.")
571 .disable_frontpage = !enable;
572 self
573 }
574
575 pub fn with_default_boot_always_attempt(mut self, enable: bool) -> Self {
577 self.config
578 .firmware
579 .uefi_config_mut()
580 .expect("Default boot always attempt is only supported for UEFI firmware.")
581 .default_boot_always_attempt = enable;
582 self
583 }
584
585 pub fn with_vmbus_redirect(mut self, enable: bool) -> Self {
587 self.config
588 .firmware
589 .openhcl_config_mut()
590 .expect("VMBus redirection is only supported for OpenHCL firmware.")
591 .vmbus_redirect = enable;
592 self
593 }
594
595 pub fn with_guest_state_lifetime(
597 mut self,
598 guest_state_lifetime: PetriGuestStateLifetime,
599 ) -> Self {
600 let disk = match self.config.vmgs {
601 PetriVmgsResource::Disk(disk)
602 | PetriVmgsResource::ReprovisionOnFailure(disk)
603 | PetriVmgsResource::Reprovision(disk) => disk,
604 PetriVmgsResource::Ephemeral => PetriVmgsDisk::default(),
605 };
606 self.config.vmgs = match guest_state_lifetime {
607 PetriGuestStateLifetime::Disk => PetriVmgsResource::Disk(disk),
608 PetriGuestStateLifetime::ReprovisionOnFailure => {
609 PetriVmgsResource::ReprovisionOnFailure(disk)
610 }
611 PetriGuestStateLifetime::Reprovision => PetriVmgsResource::Reprovision(disk),
612 PetriGuestStateLifetime::Ephemeral => {
613 if !matches!(disk.disk, PetriDiskType::Memory) {
614 panic!("attempted to use ephemeral guest state after specifying backing vmgs")
615 }
616 PetriVmgsResource::Ephemeral
617 }
618 };
619 self
620 }
621
622 pub fn with_guest_state_encryption(mut self, policy: GuestStateEncryptionPolicy) -> Self {
624 match &mut self.config.vmgs {
625 PetriVmgsResource::Disk(vmgs)
626 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
627 | PetriVmgsResource::Reprovision(vmgs) => {
628 vmgs.encryption_policy = policy;
629 }
630 PetriVmgsResource::Ephemeral => {
631 panic!("attempted to encrypt ephemeral guest state")
632 }
633 }
634 self
635 }
636
637 pub fn with_initial_vmgs(self, disk: ResolvedArtifact<impl IsTestVmgs>) -> Self {
639 self.with_backing_vmgs(PetriDiskType::Differencing(disk.into()))
640 }
641
642 pub fn with_persistent_vmgs(self, disk: impl AsRef<Path>) -> Self {
644 self.with_backing_vmgs(PetriDiskType::Persistent(disk.as_ref().to_path_buf()))
645 }
646
647 fn with_backing_vmgs(mut self, disk: PetriDiskType) -> Self {
648 match &mut self.config.vmgs {
649 PetriVmgsResource::Disk(vmgs)
650 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
651 | PetriVmgsResource::Reprovision(vmgs) => {
652 if !matches!(vmgs.disk, PetriDiskType::Memory) {
653 panic!("already specified a backing vmgs file");
654 }
655 vmgs.disk = disk;
656 }
657 PetriVmgsResource::Ephemeral => {
658 panic!("attempted to specify a backing vmgs with ephemeral guest state")
659 }
660 }
661 self
662 }
663
664 pub fn with_boot_device_type(mut self, boot: BootDeviceType) -> Self {
668 self.config.boot_device_type = boot;
669 self
670 }
671
672 pub fn with_tpm_state_persistence(mut self, tpm_state_persistence: bool) -> Self {
674 self.config.tpm_state_persistence = tpm_state_persistence;
675 self
676 }
677
678 pub fn os_flavor(&self) -> OsFlavor {
680 self.config.firmware.os_flavor()
681 }
682
683 pub fn is_openhcl(&self) -> bool {
685 self.config.firmware.is_openhcl()
686 }
687
688 pub fn isolation(&self) -> Option<IsolationType> {
690 self.config.firmware.isolation()
691 }
692
693 pub fn arch(&self) -> MachineArch {
695 self.config.arch
696 }
697
698 pub fn modify_backend(
700 mut self,
701 f: impl FnOnce(T::VmmConfig) -> T::VmmConfig + 'static + Send,
702 ) -> Self {
703 if self.modify_vmm_config.is_some() {
704 panic!("only one modify_backend allowed");
705 }
706 self.modify_vmm_config = Some(Box::new(f));
707 self
708 }
709}
710
711impl<T: PetriVmmBackend> PetriVm<T> {
712 pub async fn teardown(self) -> anyhow::Result<()> {
714 tracing::info!("Tearing down VM...");
715 self.runtime.teardown().await
716 }
717
718 pub async fn wait_for_halt(&mut self) -> anyhow::Result<PetriHaltReason> {
720 tracing::info!("Waiting for VM to halt...");
721 let halt_reason = self.runtime.wait_for_halt(false).await?;
722 tracing::info!("VM halted: {halt_reason:?}. Cancelling watchdogs...");
723 futures::future::join_all(self.watchdog_tasks.drain(..).map(|t| t.cancel())).await;
724 Ok(halt_reason)
725 }
726
727 pub async fn wait_for_clean_shutdown(&mut self) -> anyhow::Result<()> {
729 let halt_reason = self.wait_for_halt().await?;
730 if halt_reason != PetriHaltReason::PowerOff {
731 anyhow::bail!("Expected PowerOff, got {halt_reason:?}");
732 }
733 tracing::info!("VM was cleanly powered off and torn down.");
734 Ok(())
735 }
736
737 pub async fn wait_for_teardown(mut self) -> anyhow::Result<PetriHaltReason> {
740 let halt_reason = self.wait_for_halt().await?;
741 self.teardown().await?;
742 Ok(halt_reason)
743 }
744
745 pub async fn wait_for_clean_teardown(mut self) -> anyhow::Result<()> {
747 self.wait_for_clean_shutdown().await?;
748 self.teardown().await
749 }
750
751 pub async fn wait_for_reset_no_agent(&mut self) -> anyhow::Result<()> {
753 self.wait_for_reset_core().await?;
754 self.wait_for_expected_boot_event().await?;
755 Ok(())
756 }
757
758 pub async fn wait_for_reset(&mut self) -> anyhow::Result<PipetteClient> {
760 self.wait_for_reset_no_agent().await?;
761 self.wait_for_agent().await
762 }
763
764 async fn wait_for_reset_core(&mut self) -> anyhow::Result<()> {
765 tracing::info!("Waiting for VM to reset...");
766 let halt_reason = self.runtime.wait_for_halt(true).await?;
767 if halt_reason != PetriHaltReason::Reset {
768 anyhow::bail!("Expected reset, got {halt_reason:?}");
769 }
770 tracing::info!("VM reset.");
771 Ok(())
772 }
773
774 pub async fn inspect_openhcl(
785 &self,
786 path: impl Into<String>,
787 depth: Option<usize>,
788 timeout: Option<Duration>,
789 ) -> anyhow::Result<inspect::Node> {
790 self.openhcl_diag()?
791 .inspect(path.into().as_str(), depth, timeout)
792 .await
793 }
794
795 pub async fn test_inspect_openhcl(&mut self) -> anyhow::Result<()> {
797 self.inspect_openhcl("", None, None).await.map(|_| ())
798 }
799
800 pub async fn wait_for_vtl2_ready(&mut self) -> anyhow::Result<()> {
806 self.openhcl_diag()?.wait_for_vtl2().await
807 }
808
809 pub async fn kmsg(&self) -> anyhow::Result<diag_client::kmsg_stream::KmsgStream> {
811 self.openhcl_diag()?.kmsg().await
812 }
813
814 pub async fn openhcl_core_dump(&self, name: &str, path: &Path) -> anyhow::Result<()> {
817 self.openhcl_diag()?.core_dump(name, path).await
818 }
819
820 pub async fn openhcl_crash(&self, name: &str) -> anyhow::Result<()> {
822 self.openhcl_diag()?.crash(name).await
823 }
824
825 async fn wait_for_agent(&mut self) -> anyhow::Result<PipetteClient> {
828 self.runtime.wait_for_agent(false).await
829 }
830
831 pub async fn wait_for_vtl2_agent(&mut self) -> anyhow::Result<PipetteClient> {
835 self.launch_vtl2_pipette().await?;
837 self.runtime.wait_for_agent(true).await
838 }
839
840 async fn wait_for_expected_boot_event(&mut self) -> anyhow::Result<()> {
847 if let Some(expected_event) = self.expected_boot_event {
848 let event = self.wait_for_boot_event().await?;
849
850 anyhow::ensure!(
851 event == expected_event,
852 "Did not receive expected boot event"
853 );
854 } else {
855 tracing::warn!("Boot event not emitted for configured firmware or manually ignored.");
856 }
857
858 Ok(())
859 }
860
861 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent> {
864 tracing::info!("Waiting for boot event...");
865 let boot_event = loop {
866 match CancelContext::new()
867 .with_timeout(self.vmm_quirks.flaky_boot.unwrap_or(Duration::MAX))
868 .until_cancelled(self.runtime.wait_for_boot_event())
869 .await
870 {
871 Ok(res) => break res?,
872 Err(_) => {
873 tracing::error!("Did not get boot event in required time, resetting...");
874 if let Some(inspector) = self.runtime.inspector() {
875 save_inspect(
876 "vmm",
877 Box::pin(async move { inspector.inspect_all().await }),
878 &self.resources.log_source,
879 )
880 .await;
881 }
882
883 self.runtime.reset().await?;
884 continue;
885 }
886 }
887 };
888 tracing::info!("Got boot event: {boot_event:?}");
889 Ok(boot_event)
890 }
891
892 pub async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()> {
895 tracing::info!("Waiting for enlightened shutdown to be ready");
896 self.runtime.wait_for_enlightened_shutdown_ready().await?;
897
898 let mut wait_time = Duration::from_secs(10);
904
905 if let Some(duration) = self.guest_quirks.hyperv_shutdown_ic_sleep {
907 wait_time += duration;
908 }
909
910 tracing::info!(
911 "Shutdown IC reported ready, waiting for an extra {}s",
912 wait_time.as_secs()
913 );
914 PolledTimer::new(&self.resources.driver)
915 .sleep(wait_time)
916 .await;
917
918 tracing::info!("Sending enlightened shutdown command");
919 self.runtime.send_enlightened_shutdown(kind).await
920 }
921
922 pub async fn restart_openhcl(
925 &mut self,
926 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
927 flags: OpenHclServicingFlags,
928 ) -> anyhow::Result<()> {
929 self.runtime
930 .restart_openhcl(&new_openhcl.erase(), flags)
931 .await
932 }
933
934 pub async fn save_openhcl(
937 &mut self,
938 new_openhcl: ResolvedArtifact<impl IsOpenhclIgvm>,
939 flags: OpenHclServicingFlags,
940 ) -> anyhow::Result<()> {
941 self.runtime.save_openhcl(&new_openhcl.erase(), flags).await
942 }
943
944 pub async fn restore_openhcl(&mut self) -> anyhow::Result<()> {
947 self.runtime.restore_openhcl().await
948 }
949
950 pub fn arch(&self) -> MachineArch {
952 self.arch
953 }
954
955 pub fn backend(&mut self) -> &mut T::VmRuntime {
957 &mut self.runtime
958 }
959
960 async fn launch_vtl2_pipette(&self) -> anyhow::Result<()> {
961 let res = self
963 .openhcl_diag()?
964 .run_vtl2_command(
965 "sh",
966 &[
967 "-c",
968 "mkdir /cidata && mount LABEL=cidata /cidata && sh -c '/cidata/pipette &'",
969 ],
970 )
971 .await?;
972
973 if !res.exit_status.success() {
974 anyhow::bail!("Failed to start VTL 2 pipette: {:?}", res);
975 }
976
977 Ok(())
978 }
979
980 fn openhcl_diag(&self) -> anyhow::Result<&OpenHclDiagHandler> {
981 if let Some(ohd) = self.openhcl_diag_handler.as_ref() {
982 Ok(ohd)
983 } else {
984 anyhow::bail!("VM is not configured with OpenHCL")
985 }
986 }
987
988 pub async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
990 self.runtime.get_guest_state_file().await
991 }
992}
993
994#[async_trait]
996pub trait PetriVmRuntime: Send + Sync + 'static {
997 type VmInspector: PetriVmInspector;
999 type VmFramebufferAccess: PetriVmFramebufferAccess;
1001
1002 async fn teardown(self) -> anyhow::Result<()>;
1004 async fn wait_for_halt(&mut self, allow_reset: bool) -> anyhow::Result<PetriHaltReason>;
1007 async fn wait_for_agent(&mut self, set_high_vtl: bool) -> anyhow::Result<PipetteClient>;
1009 fn openhcl_diag(&self) -> Option<OpenHclDiagHandler>;
1011 async fn wait_for_boot_event(&mut self) -> anyhow::Result<FirmwareEvent>;
1014 async fn wait_for_enlightened_shutdown_ready(&mut self) -> anyhow::Result<()>;
1017 async fn send_enlightened_shutdown(&mut self, kind: ShutdownKind) -> anyhow::Result<()>;
1019 async fn restart_openhcl(
1022 &mut self,
1023 new_openhcl: &ResolvedArtifact,
1024 flags: OpenHclServicingFlags,
1025 ) -> anyhow::Result<()>;
1026 async fn save_openhcl(
1030 &mut self,
1031 new_openhcl: &ResolvedArtifact,
1032 flags: OpenHclServicingFlags,
1033 ) -> anyhow::Result<()>;
1034 async fn restore_openhcl(&mut self) -> anyhow::Result<()>;
1037 fn inspector(&self) -> Option<Self::VmInspector> {
1039 None
1040 }
1041 fn take_framebuffer_access(&mut self) -> Option<Self::VmFramebufferAccess> {
1044 None
1045 }
1046 async fn reset(&mut self) -> anyhow::Result<()>;
1048 async fn get_guest_state_file(&self) -> anyhow::Result<Option<PathBuf>> {
1050 Ok(None)
1051 }
1052}
1053
1054#[async_trait]
1056pub trait PetriVmInspector: Send + Sync + 'static {
1057 async fn inspect_all(&self) -> anyhow::Result<inspect::Node>;
1059}
1060
1061pub struct NoPetriVmInspector;
1063#[async_trait]
1064impl PetriVmInspector for NoPetriVmInspector {
1065 async fn inspect_all(&self) -> anyhow::Result<inspect::Node> {
1066 unreachable!()
1067 }
1068}
1069
1070pub struct VmScreenshotMeta {
1072 pub color: image::ExtendedColorType,
1074 pub width: u16,
1076 pub height: u16,
1078}
1079
1080#[async_trait]
1082pub trait PetriVmFramebufferAccess: Send + 'static {
1083 async fn screenshot(&mut self, image: &mut Vec<u8>)
1086 -> anyhow::Result<Option<VmScreenshotMeta>>;
1087}
1088
1089pub struct NoPetriVmFramebufferAccess;
1091#[async_trait]
1092impl PetriVmFramebufferAccess for NoPetriVmFramebufferAccess {
1093 async fn screenshot(
1094 &mut self,
1095 _image: &mut Vec<u8>,
1096 ) -> anyhow::Result<Option<VmScreenshotMeta>> {
1097 unreachable!()
1098 }
1099}
1100
1101pub struct ProcessorTopology {
1103 pub vp_count: u32,
1105 pub enable_smt: Option<bool>,
1107 pub vps_per_socket: Option<u32>,
1109 pub apic_mode: Option<ApicMode>,
1111}
1112
1113impl Default for ProcessorTopology {
1114 fn default() -> Self {
1115 Self {
1116 vp_count: 2,
1117 enable_smt: None,
1118 vps_per_socket: None,
1119 apic_mode: None,
1120 }
1121 }
1122}
1123
1124#[derive(Debug, Clone, Copy)]
1126pub enum ApicMode {
1127 Xapic,
1129 X2apicSupported,
1131 X2apicEnabled,
1133}
1134
1135pub struct MemoryConfig {
1137 pub startup_bytes: u64,
1140 pub dynamic_memory_range: Option<(u64, u64)>,
1144}
1145
1146impl Default for MemoryConfig {
1147 fn default() -> Self {
1148 Self {
1149 startup_bytes: 0x1_0000_0000,
1150 dynamic_memory_range: None,
1151 }
1152 }
1153}
1154
1155#[derive(Debug)]
1157pub struct UefiConfig {
1158 pub secure_boot_enabled: bool,
1160 pub secure_boot_template: Option<SecureBootTemplate>,
1162 pub disable_frontpage: bool,
1164 pub default_boot_always_attempt: bool,
1166}
1167
1168impl Default for UefiConfig {
1169 fn default() -> Self {
1170 Self {
1171 secure_boot_enabled: false,
1172 secure_boot_template: None,
1173 disable_frontpage: true,
1174 default_boot_always_attempt: false,
1175 }
1176 }
1177}
1178
1179#[derive(Debug, Clone)]
1181pub enum OpenHclLogConfig {
1182 TestDefault,
1186 BuiltInDefault,
1189 Custom(String),
1192}
1193
1194#[derive(Debug, Clone)]
1196pub struct OpenHclConfig {
1197 pub vtl2_nvme_boot: bool,
1200 pub vmbus_redirect: bool,
1202 pub command_line: Option<String>,
1206 pub log_levels: OpenHclLogConfig,
1210 pub vtl2_base_address_type: Option<Vtl2BaseAddressType>,
1213}
1214
1215impl OpenHclConfig {
1216 pub fn command_line(&self) -> String {
1219 let mut cmdline = self.command_line.clone();
1220 match &self.log_levels {
1221 OpenHclLogConfig::TestDefault => {
1222 let default_log_levels = {
1223 let openhcl_tracing = if let Ok(x) =
1225 std::env::var("OPENVMM_LOG").or_else(|_| std::env::var("HVLITE_LOG"))
1226 {
1227 format!("OPENVMM_LOG={x}")
1228 } else {
1229 "OPENVMM_LOG=debug".to_owned()
1230 };
1231 let openhcl_show_spans = if let Ok(x) = std::env::var("OPENVMM_SHOW_SPANS") {
1232 format!("OPENVMM_SHOW_SPANS={x}")
1233 } else {
1234 "OPENVMM_SHOW_SPANS=true".to_owned()
1235 };
1236 format!("{openhcl_tracing} {openhcl_show_spans}")
1237 };
1238 append_cmdline(&mut cmdline, &default_log_levels);
1239 }
1240 OpenHclLogConfig::BuiltInDefault => {
1241 }
1243 OpenHclLogConfig::Custom(levels) => {
1244 append_cmdline(&mut cmdline, levels);
1245 }
1246 }
1247
1248 cmdline.unwrap_or_default()
1249 }
1250}
1251
1252impl Default for OpenHclConfig {
1253 fn default() -> Self {
1254 Self {
1255 vtl2_nvme_boot: false,
1256 vmbus_redirect: false,
1257 command_line: None,
1258 log_levels: OpenHclLogConfig::TestDefault,
1259 vtl2_base_address_type: None,
1260 }
1261 }
1262}
1263
1264#[derive(Debug)]
1266pub enum Firmware {
1267 LinuxDirect {
1269 kernel: ResolvedArtifact,
1271 initrd: ResolvedArtifact,
1273 },
1274 OpenhclLinuxDirect {
1276 igvm_path: ResolvedArtifact,
1278 openhcl_config: OpenHclConfig,
1280 },
1281 Pcat {
1283 guest: PcatGuest,
1285 bios_firmware: ResolvedOptionalArtifact,
1287 svga_firmware: ResolvedOptionalArtifact,
1289 },
1290 OpenhclPcat {
1292 guest: PcatGuest,
1294 igvm_path: ResolvedArtifact,
1296 bios_firmware: ResolvedOptionalArtifact,
1298 svga_firmware: ResolvedOptionalArtifact,
1300 openhcl_config: OpenHclConfig,
1302 },
1303 Uefi {
1305 guest: UefiGuest,
1307 uefi_firmware: ResolvedArtifact,
1309 uefi_config: UefiConfig,
1311 },
1312 OpenhclUefi {
1314 guest: UefiGuest,
1316 isolation: Option<IsolationType>,
1318 igvm_path: ResolvedArtifact,
1320 uefi_config: UefiConfig,
1322 openhcl_config: OpenHclConfig,
1324 },
1325}
1326
1327#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1329pub enum BootDeviceType {
1330 None,
1332 Ide,
1334 Scsi,
1336 Nvme,
1338}
1339
1340impl Firmware {
1341 pub fn linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
1343 use petri_artifacts_vmm_test::artifacts::loadable::*;
1344 match arch {
1345 MachineArch::X86_64 => Firmware::LinuxDirect {
1346 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_X64).erase(),
1347 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_X64).erase(),
1348 },
1349 MachineArch::Aarch64 => Firmware::LinuxDirect {
1350 kernel: resolver.require(LINUX_DIRECT_TEST_KERNEL_AARCH64).erase(),
1351 initrd: resolver.require(LINUX_DIRECT_TEST_INITRD_AARCH64).erase(),
1352 },
1353 }
1354 }
1355
1356 pub fn openhcl_linux_direct(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
1358 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
1359 match arch {
1360 MachineArch::X86_64 => Firmware::OpenhclLinuxDirect {
1361 igvm_path: resolver.require(LATEST_LINUX_DIRECT_TEST_X64).erase(),
1362 openhcl_config: Default::default(),
1363 },
1364 MachineArch::Aarch64 => todo!("Linux direct not yet supported on aarch64"),
1365 }
1366 }
1367
1368 pub fn pcat(resolver: &ArtifactResolver<'_>, guest: PcatGuest) -> Self {
1370 use petri_artifacts_vmm_test::artifacts::loadable::*;
1371 Firmware::Pcat {
1372 guest,
1373 bios_firmware: resolver.try_require(PCAT_FIRMWARE_X64).erase(),
1374 svga_firmware: resolver.try_require(SVGA_FIRMWARE_X64).erase(),
1375 }
1376 }
1377
1378 pub fn uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch, guest: UefiGuest) -> Self {
1380 use petri_artifacts_vmm_test::artifacts::loadable::*;
1381 let uefi_firmware = match arch {
1382 MachineArch::X86_64 => resolver.require(UEFI_FIRMWARE_X64).erase(),
1383 MachineArch::Aarch64 => resolver.require(UEFI_FIRMWARE_AARCH64).erase(),
1384 };
1385 Firmware::Uefi {
1386 guest,
1387 uefi_firmware,
1388 uefi_config: Default::default(),
1389 }
1390 }
1391
1392 pub fn openhcl_uefi(
1394 resolver: &ArtifactResolver<'_>,
1395 arch: MachineArch,
1396 guest: UefiGuest,
1397 isolation: Option<IsolationType>,
1398 vtl2_nvme_boot: bool,
1399 ) -> Self {
1400 use petri_artifacts_vmm_test::artifacts::openhcl_igvm::*;
1401 let igvm_path = match arch {
1402 MachineArch::X86_64 if isolation.is_some() => resolver.require(LATEST_CVM_X64).erase(),
1403 MachineArch::X86_64 => resolver.require(LATEST_STANDARD_X64).erase(),
1404 MachineArch::Aarch64 => resolver.require(LATEST_STANDARD_AARCH64).erase(),
1405 };
1406 Firmware::OpenhclUefi {
1407 guest,
1408 isolation,
1409 igvm_path,
1410 uefi_config: Default::default(),
1411 openhcl_config: OpenHclConfig {
1412 vtl2_nvme_boot,
1413 ..Default::default()
1414 },
1415 }
1416 }
1417
1418 fn is_openhcl(&self) -> bool {
1419 match self {
1420 Firmware::OpenhclLinuxDirect { .. }
1421 | Firmware::OpenhclUefi { .. }
1422 | Firmware::OpenhclPcat { .. } => true,
1423 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => false,
1424 }
1425 }
1426
1427 fn isolation(&self) -> Option<IsolationType> {
1428 match self {
1429 Firmware::OpenhclUefi { isolation, .. } => *isolation,
1430 Firmware::LinuxDirect { .. }
1431 | Firmware::Pcat { .. }
1432 | Firmware::Uefi { .. }
1433 | Firmware::OpenhclLinuxDirect { .. }
1434 | Firmware::OpenhclPcat { .. } => None,
1435 }
1436 }
1437
1438 fn is_linux_direct(&self) -> bool {
1439 match self {
1440 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => true,
1441 Firmware::Pcat { .. }
1442 | Firmware::Uefi { .. }
1443 | Firmware::OpenhclUefi { .. }
1444 | Firmware::OpenhclPcat { .. } => false,
1445 }
1446 }
1447
1448 fn is_pcat(&self) -> bool {
1449 match self {
1450 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => true,
1451 Firmware::Uefi { .. }
1452 | Firmware::OpenhclUefi { .. }
1453 | Firmware::LinuxDirect { .. }
1454 | Firmware::OpenhclLinuxDirect { .. } => false,
1455 }
1456 }
1457
1458 fn os_flavor(&self) -> OsFlavor {
1459 match self {
1460 Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => OsFlavor::Linux,
1461 Firmware::Uefi {
1462 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
1463 ..
1464 }
1465 | Firmware::OpenhclUefi {
1466 guest: UefiGuest::GuestTestUefi { .. } | UefiGuest::None,
1467 ..
1468 } => OsFlavor::Uefi,
1469 Firmware::Pcat {
1470 guest: PcatGuest::Vhd(cfg),
1471 ..
1472 }
1473 | Firmware::OpenhclPcat {
1474 guest: PcatGuest::Vhd(cfg),
1475 ..
1476 }
1477 | Firmware::Uefi {
1478 guest: UefiGuest::Vhd(cfg),
1479 ..
1480 }
1481 | Firmware::OpenhclUefi {
1482 guest: UefiGuest::Vhd(cfg),
1483 ..
1484 } => cfg.os_flavor,
1485 Firmware::Pcat {
1486 guest: PcatGuest::Iso(cfg),
1487 ..
1488 }
1489 | Firmware::OpenhclPcat {
1490 guest: PcatGuest::Iso(cfg),
1491 ..
1492 } => cfg.os_flavor,
1493 }
1494 }
1495
1496 fn quirks(&self) -> GuestQuirks {
1497 match self {
1498 Firmware::Pcat {
1499 guest: PcatGuest::Vhd(cfg),
1500 ..
1501 }
1502 | Firmware::Uefi {
1503 guest: UefiGuest::Vhd(cfg),
1504 ..
1505 }
1506 | Firmware::OpenhclUefi {
1507 guest: UefiGuest::Vhd(cfg),
1508 ..
1509 } => cfg.quirks.clone(),
1510 Firmware::Pcat {
1511 guest: PcatGuest::Iso(cfg),
1512 ..
1513 } => cfg.quirks.clone(),
1514 _ => Default::default(),
1515 }
1516 }
1517
1518 fn expected_boot_event(&self) -> Option<FirmwareEvent> {
1519 match self {
1520 Firmware::LinuxDirect { .. }
1521 | Firmware::OpenhclLinuxDirect { .. }
1522 | Firmware::Uefi {
1523 guest: UefiGuest::GuestTestUefi(_),
1524 ..
1525 }
1526 | Firmware::OpenhclUefi {
1527 guest: UefiGuest::GuestTestUefi(_),
1528 ..
1529 } => None,
1530 Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => {
1531 Some(FirmwareEvent::BootAttempt)
1533 }
1534 Firmware::Uefi {
1535 guest: UefiGuest::None,
1536 ..
1537 }
1538 | Firmware::OpenhclUefi {
1539 guest: UefiGuest::None,
1540 ..
1541 } => Some(FirmwareEvent::NoBootDevice),
1542 Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => {
1543 Some(FirmwareEvent::BootSuccess)
1544 }
1545 }
1546 }
1547
1548 fn openhcl_config(&self) -> Option<&OpenHclConfig> {
1549 match self {
1550 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
1551 | Firmware::OpenhclUefi { openhcl_config, .. }
1552 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
1553 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
1554 }
1555 }
1556
1557 fn openhcl_config_mut(&mut self) -> Option<&mut OpenHclConfig> {
1558 match self {
1559 Firmware::OpenhclLinuxDirect { openhcl_config, .. }
1560 | Firmware::OpenhclUefi { openhcl_config, .. }
1561 | Firmware::OpenhclPcat { openhcl_config, .. } => Some(openhcl_config),
1562 Firmware::LinuxDirect { .. } | Firmware::Pcat { .. } | Firmware::Uefi { .. } => None,
1563 }
1564 }
1565
1566 fn uefi_config(&self) -> Option<&UefiConfig> {
1567 match self {
1568 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
1569 Some(uefi_config)
1570 }
1571 Firmware::LinuxDirect { .. }
1572 | Firmware::OpenhclLinuxDirect { .. }
1573 | Firmware::Pcat { .. }
1574 | Firmware::OpenhclPcat { .. } => None,
1575 }
1576 }
1577
1578 fn uefi_config_mut(&mut self) -> Option<&mut UefiConfig> {
1579 match self {
1580 Firmware::Uefi { uefi_config, .. } | Firmware::OpenhclUefi { uefi_config, .. } => {
1581 Some(uefi_config)
1582 }
1583 Firmware::LinuxDirect { .. }
1584 | Firmware::OpenhclLinuxDirect { .. }
1585 | Firmware::Pcat { .. }
1586 | Firmware::OpenhclPcat { .. } => None,
1587 }
1588 }
1589}
1590
1591#[derive(Debug)]
1594pub enum PcatGuest {
1595 Vhd(BootImageConfig<boot_image_type::Vhd>),
1597 Iso(BootImageConfig<boot_image_type::Iso>),
1599}
1600
1601impl PcatGuest {
1602 fn artifact(&self) -> &ResolvedArtifact {
1603 match self {
1604 PcatGuest::Vhd(disk) => &disk.artifact,
1605 PcatGuest::Iso(disk) => &disk.artifact,
1606 }
1607 }
1608}
1609
1610#[derive(Debug)]
1613pub enum UefiGuest {
1614 Vhd(BootImageConfig<boot_image_type::Vhd>),
1616 GuestTestUefi(ResolvedArtifact),
1618 None,
1620}
1621
1622impl UefiGuest {
1623 pub fn guest_test_uefi(resolver: &ArtifactResolver<'_>, arch: MachineArch) -> Self {
1625 use petri_artifacts_vmm_test::artifacts::test_vhd::*;
1626 let artifact = match arch {
1627 MachineArch::X86_64 => resolver.require(GUEST_TEST_UEFI_X64).erase(),
1628 MachineArch::Aarch64 => resolver.require(GUEST_TEST_UEFI_AARCH64).erase(),
1629 };
1630 UefiGuest::GuestTestUefi(artifact)
1631 }
1632
1633 fn artifact(&self) -> Option<&ResolvedArtifact> {
1634 match self {
1635 UefiGuest::Vhd(vhd) => Some(&vhd.artifact),
1636 UefiGuest::GuestTestUefi(p) => Some(p),
1637 UefiGuest::None => None,
1638 }
1639 }
1640}
1641
1642pub mod boot_image_type {
1644 mod private {
1645 pub trait Sealed {}
1646 impl Sealed for super::Vhd {}
1647 impl Sealed for super::Iso {}
1648 }
1649
1650 pub trait BootImageType: private::Sealed {}
1653
1654 #[derive(Debug)]
1656 pub enum Vhd {}
1657
1658 #[derive(Debug)]
1660 pub enum Iso {}
1661
1662 impl BootImageType for Vhd {}
1663 impl BootImageType for Iso {}
1664}
1665
1666#[derive(Debug)]
1668pub struct BootImageConfig<T: boot_image_type::BootImageType> {
1669 artifact: ResolvedArtifact,
1671 os_flavor: OsFlavor,
1673 quirks: GuestQuirks,
1677 _type: core::marker::PhantomData<T>,
1679}
1680
1681impl BootImageConfig<boot_image_type::Vhd> {
1682 pub fn from_vhd<A>(artifact: ResolvedArtifact<A>) -> Self
1684 where
1685 A: petri_artifacts_common::tags::IsTestVhd,
1686 {
1687 BootImageConfig {
1688 artifact: artifact.erase(),
1689 os_flavor: A::OS_FLAVOR,
1690 quirks: A::quirks(),
1691 _type: std::marker::PhantomData,
1692 }
1693 }
1694}
1695
1696impl BootImageConfig<boot_image_type::Iso> {
1697 pub fn from_iso<A>(artifact: ResolvedArtifact<A>) -> Self
1699 where
1700 A: petri_artifacts_common::tags::IsTestIso,
1701 {
1702 BootImageConfig {
1703 artifact: artifact.erase(),
1704 os_flavor: A::OS_FLAVOR,
1705 quirks: A::quirks(),
1706 _type: std::marker::PhantomData,
1707 }
1708 }
1709}
1710
1711#[derive(Debug, Clone, Copy)]
1713pub enum IsolationType {
1714 Vbs,
1716 Snp,
1718 Tdx,
1720}
1721
1722#[derive(Default, Debug, Clone, Copy)]
1724pub struct OpenHclServicingFlags {
1725 pub enable_nvme_keepalive: bool,
1727 pub override_version_checks: bool,
1729 pub stop_timeout_hint_secs: Option<u16>,
1731}
1732
1733#[derive(Debug, Clone)]
1735pub enum PetriDiskType {
1736 Memory,
1738 Differencing(PathBuf),
1740 Persistent(PathBuf),
1742}
1743
1744#[derive(Debug, Clone)]
1746pub struct PetriVmgsDisk {
1747 pub disk: PetriDiskType,
1749 pub encryption_policy: GuestStateEncryptionPolicy,
1751}
1752
1753impl Default for PetriVmgsDisk {
1754 fn default() -> Self {
1755 PetriVmgsDisk {
1756 disk: PetriDiskType::Memory,
1757 encryption_policy: GuestStateEncryptionPolicy::None(false),
1759 }
1760 }
1761}
1762
1763#[derive(Debug, Clone)]
1765pub enum PetriVmgsResource {
1766 Disk(PetriVmgsDisk),
1768 ReprovisionOnFailure(PetriVmgsDisk),
1770 Reprovision(PetriVmgsDisk),
1772 Ephemeral,
1774}
1775
1776impl PetriVmgsResource {
1777 pub fn disk(&self) -> Option<&PetriVmgsDisk> {
1779 match self {
1780 PetriVmgsResource::Disk(vmgs)
1781 | PetriVmgsResource::ReprovisionOnFailure(vmgs)
1782 | PetriVmgsResource::Reprovision(vmgs) => Some(vmgs),
1783 PetriVmgsResource::Ephemeral => None,
1784 }
1785 }
1786}
1787
1788#[derive(Debug, Clone, Copy)]
1790pub enum PetriGuestStateLifetime {
1791 Disk,
1794 ReprovisionOnFailure,
1796 Reprovision,
1798 Ephemeral,
1800}
1801
1802#[derive(Debug, Clone, Copy)]
1804pub enum SecureBootTemplate {
1805 MicrosoftWindows,
1807 MicrosoftUefiCertificateAuthority,
1809}
1810
1811#[derive(Default, Debug, Clone)]
1814pub struct VmmQuirks {
1815 pub flaky_boot: Option<Duration>,
1818}
1819
1820fn make_vm_safe_name(name: &str) -> String {
1826 const MAX_VM_NAME_LENGTH: usize = 100;
1827 const HASH_LENGTH: usize = 4;
1828 const MAX_PREFIX_LENGTH: usize = MAX_VM_NAME_LENGTH - HASH_LENGTH;
1829
1830 if name.len() <= MAX_VM_NAME_LENGTH {
1831 name.to_owned()
1832 } else {
1833 let mut hasher = DefaultHasher::new();
1835 name.hash(&mut hasher);
1836 let hash = hasher.finish();
1837
1838 let hash_suffix = format!("{:04x}", hash & 0xFFFF);
1840
1841 let truncated = &name[..MAX_PREFIX_LENGTH];
1843 tracing::debug!(
1844 "VM name too long ({}), truncating '{}' to '{}{}'",
1845 name.len(),
1846 name,
1847 truncated,
1848 hash_suffix
1849 );
1850
1851 format!("{}{}", truncated, hash_suffix)
1852 }
1853}
1854
1855#[derive(Debug, Clone, Copy, Eq, PartialEq)]
1857pub enum PetriHaltReason {
1858 PowerOff,
1860 Reset,
1862 Hibernate,
1864 TripleFault,
1866 Other,
1868}
1869
1870fn append_cmdline(cmd: &mut Option<String>, add_cmd: impl AsRef<str>) {
1871 if let Some(cmd) = cmd.as_mut() {
1872 cmd.push(' ');
1873 cmd.push_str(add_cmd.as_ref());
1874 } else {
1875 *cmd = Some(add_cmd.as_ref().to_string());
1876 }
1877}
1878
1879async fn save_inspect(
1880 name: &str,
1881 inspect: std::pin::Pin<Box<dyn Future<Output = anyhow::Result<inspect::Node>> + Send>>,
1882 log_source: &PetriLogSource,
1883) {
1884 tracing::info!("Collecting {name} inspect details.");
1885 let node = match inspect.await {
1886 Ok(n) => n,
1887 Err(e) => {
1888 tracing::error!(?e, "Failed to get {name}");
1889 return;
1890 }
1891 };
1892 if let Err(e) =
1893 log_source.write_attachment(&format!("timeout_inspect_{name}.log"), format!("{node:#}"))
1894 {
1895 tracing::error!(?e, "Failed to save {name} inspect log");
1896 return;
1897 }
1898 tracing::info!("{name} inspect task finished.");
1899}
1900
1901#[cfg(test)]
1902mod tests {
1903 use super::make_vm_safe_name;
1904
1905 #[test]
1906 fn test_short_names_unchanged() {
1907 let short_name = "short_test_name";
1908 assert_eq!(make_vm_safe_name(short_name), short_name);
1909 }
1910
1911 #[test]
1912 fn test_exactly_100_chars_unchanged() {
1913 let name_100 = "a".repeat(100);
1914 assert_eq!(make_vm_safe_name(&name_100), name_100);
1915 }
1916
1917 #[test]
1918 fn test_long_name_truncated() {
1919 let long_name = "multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_openhcl_servicing";
1920 let result = make_vm_safe_name(long_name);
1921
1922 assert_eq!(result.len(), 100);
1924
1925 assert!(result.starts_with("multiarch::openhcl_servicing::hyperv_openhcl_uefi_aarch64_ubuntu_2404_server_aarch64_ope"));
1927
1928 let suffix = &result[96..];
1930 assert_eq!(suffix.len(), 4);
1931 assert!(u16::from_str_radix(suffix, 16).is_ok());
1933 }
1934
1935 #[test]
1936 fn test_deterministic_results() {
1937 let long_name = "very_long_test_name_that_exceeds_the_100_character_limit_and_should_be_truncated_consistently_every_time";
1938 let result1 = make_vm_safe_name(long_name);
1939 let result2 = make_vm_safe_name(long_name);
1940
1941 assert_eq!(result1, result2);
1942 assert_eq!(result1.len(), 100);
1943 }
1944
1945 #[test]
1946 fn test_different_names_different_hashes() {
1947 let name1 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_1";
1948 let name2 = "very_long_test_name_that_definitely_exceeds_the_100_character_limit_and_should_be_truncated_by_the_function_version_2";
1949
1950 let result1 = make_vm_safe_name(name1);
1951 let result2 = make_vm_safe_name(name2);
1952
1953 assert_eq!(result1.len(), 100);
1955 assert_eq!(result2.len(), 100);
1956
1957 assert_ne!(result1, result2);
1959 assert_ne!(&result1[96..], &result2[96..]);
1960 }
1961}