Skip to main content

petri/vm/openvmm/
construct.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Contains [`PetriVmConfigOpenVmm::new`], which builds a [`PetriVmConfigOpenVmm`] with all
5//! default settings for a given [`Firmware`] and [`MachineArch`].
6
7use super::PetriVmConfigOpenVmm;
8use super::PetriVmResourcesOpenVmm;
9use crate::Drive;
10use crate::EfiDiagnosticsLogLevel;
11use crate::Firmware;
12use crate::IsolationType;
13use crate::MemoryConfig;
14use crate::OpenHclConfig;
15use crate::PcieNvmeDrive;
16use crate::PetriLogSource;
17use crate::PetriVmConfig;
18use crate::PetriVmResources;
19use crate::PetriVmgsResource;
20use crate::ProcessorTopology;
21use crate::SecureBootTemplate;
22use crate::TpmConfig;
23use crate::UefiConfig;
24use crate::VmbusStorageType;
25use crate::linux_direct_serial_agent::LinuxDirectSerialAgent;
26
27use crate::SIZE_1_MB;
28use crate::VmbusStorageController;
29use crate::openvmm::memdiff_vmgs;
30use crate::openvmm::petri_disk_to_openvmm;
31use crate::vm::PetriVmProperties;
32use crate::vm::append_cmdline;
33use anyhow::Context;
34use framebuffer::FRAMEBUFFER_SIZE;
35use framebuffer::Framebuffer;
36use framebuffer::FramebufferAccess;
37use fs_err::File;
38use futures::StreamExt;
39use get_resources::crash::GuestCrashDeviceHandle;
40use get_resources::ged::FirmwareEvent;
41use guid::Guid;
42use hyperv_ic_resources::shutdown::ShutdownIcHandle;
43use ide_resources::GuestMedia;
44use ide_resources::IdeDeviceConfig;
45use mesh_process::Mesh;
46use nvme_resources::NamespaceDefinition;
47use nvme_resources::NvmeControllerHandle;
48use openvmm_defs::config::Config;
49use openvmm_defs::config::DEFAULT_PCAT_BOOT_ORDER;
50use openvmm_defs::config::DeviceVtl;
51use openvmm_defs::config::HypervisorConfig;
52use openvmm_defs::config::LateMapVtl0MemoryPolicy;
53use openvmm_defs::config::LoadMode;
54use openvmm_defs::config::NumaNode;
55use openvmm_defs::config::NumaTopology;
56use openvmm_defs::config::PcieDeviceConfig;
57use openvmm_defs::config::ProcessorTopologyConfig;
58use openvmm_defs::config::SerialInformation;
59use openvmm_defs::config::VmbusConfig;
60use openvmm_defs::config::VpAssignment;
61use openvmm_defs::config::VpciDeviceConfig;
62use openvmm_defs::config::Vtl2BaseAddressType;
63use openvmm_defs::config::Vtl2Config;
64use openvmm_pcat_locator::RomFileLocation;
65use pal_async::DefaultDriver;
66use pal_async::socket::PolledSocket;
67use pal_async::task::Spawn;
68use pal_async::task::Task;
69use petri_artifacts_common::tags::MachineArch;
70use petri_artifacts_core::ResolvedArtifact;
71use pipette_client::PIPETTE_PORT;
72use scsidisk_resources::SimpleScsiDiskHandle;
73use scsidisk_resources::SimpleScsiDvdHandle;
74use serial_16550_resources::ComPort;
75use serial_core::resources::DisconnectedSerialBackendHandle;
76use serial_socket::net::OpenSocketSerialConfig;
77use sparse_mmap::alloc_shared_memory;
78use std::collections::HashMap;
79use storvsp_resources::ScsiControllerHandle;
80use storvsp_resources::ScsiDeviceAndPath;
81use storvsp_resources::ScsiPath;
82use tempfile::TempPath;
83use tpm_resources::TpmDeviceHandle;
84use tpm_resources::TpmRegisterLayout;
85use uidevices_resources::SynthVideoHandle;
86use unix_socket::UnixListener;
87use unix_socket::UnixStream;
88use video_core::SharedFramebufferHandle;
89use virtio_resources::VirtioPciDeviceHandle;
90use virtio_resources::blk::VirtioBlkHandle;
91use virtio_resources::vsock::VirtioVsockHandle;
92use vm_manifest_builder::VmChipsetResult;
93use vm_manifest_builder::VmManifestBuilder;
94use vm_resource::IntoResource;
95use vm_resource::Resource;
96use vm_resource::kind::SerialBackendHandle;
97use vm_resource::kind::VmbusDeviceHandleKind;
98use vmbus_serial_resources::VmbusSerialDeviceHandle;
99use vmbus_serial_resources::VmbusSerialPort;
100use vmcore::non_volatile_store::resources::EphemeralNonVolatileStoreHandle;
101use vmgs_resources::GuestStateEncryptionPolicy;
102use vmgs_resources::VmgsFileHandle;
103use vmotherboard::ChipsetDeviceHandle;
104
105impl PetriVmConfigOpenVmm {
106    /// Create a new VM configuration.
107    pub async fn new(
108        openvmm_path: &ResolvedArtifact,
109        petri_vm_config: PetriVmConfig,
110        resources: &PetriVmResources,
111        properties: PetriVmProperties,
112    ) -> anyhow::Result<Self> {
113        let PetriVmConfig {
114            name: _,
115            arch,
116            host_log_levels,
117            firmware,
118            memory,
119            proc_topology,
120            vmgs,
121            tpm: tpm_config,
122            vmbus_storage_controllers,
123            pcie_nvme_drives,
124            physical_nvme_devices,
125        } = petri_vm_config;
126
127        if !physical_nvme_devices.is_empty() {
128            anyhow::bail!("Physical NVMe devices are only supported with the Hyper-V backend");
129        }
130
131        tracing::debug!(?firmware, ?arch, "Petri VM firmware configuration");
132
133        let PetriVmResources { driver, log_source } = resources;
134
135        let mesh = Mesh::new("petri_mesh".to_string())?;
136
137        let setup = PetriVmConfigSetupCore {
138            arch,
139            firmware: &firmware,
140            driver,
141            logger: log_source,
142            vmgs: &vmgs,
143            tpm_config: tpm_config.as_ref(),
144            mesh: &mesh,
145            openvmm_path,
146            uses_pipette_as_init: properties.uses_pipette_as_init,
147            enable_serial: properties.enable_serial,
148            use_virtio_vsock: properties.use_virtio_vsock,
149            no_vmbus: properties.no_vmbus,
150        };
151
152        let mut chipset = VmManifestBuilder::new(
153            match firmware {
154                Firmware::LinuxDirect { .. } => {
155                    vm_manifest_builder::BaseChipsetType::HyperVGen2LinuxDirect
156                }
157                Firmware::OpenhclLinuxDirect { .. } => {
158                    vm_manifest_builder::BaseChipsetType::HclHost
159                }
160                Firmware::OpenhclUefi { .. } => vm_manifest_builder::BaseChipsetType::HclHost,
161                Firmware::Pcat { .. } => vm_manifest_builder::BaseChipsetType::HypervGen1,
162                Firmware::Uefi { .. } => vm_manifest_builder::BaseChipsetType::HypervGen2Uefi,
163                Firmware::OpenhclPcat { .. } => todo!("OpenVMM OpenHCL PCAT"),
164            },
165            match arch {
166                MachineArch::X86_64 => vm_manifest_builder::MachineArch::X86_64,
167                MachineArch::Aarch64 => vm_manifest_builder::MachineArch::Aarch64,
168            },
169        );
170
171        let mut load_mode = setup.load_firmware()?;
172
173        // If using pipette-as-init, replace the initrd with the pre-built
174        // one that has pipette injected. run_core() guarantees that
175        // prebuilt_initrd is set when uses_pipette_as_init is true.
176        if properties.uses_pipette_as_init {
177            if let LoadMode::Linux { initrd, .. } = &mut load_mode {
178                let prebuilt = properties
179                    .prebuilt_initrd
180                    .as_ref()
181                    .expect("uses_pipette_as_init requires prebuilt_initrd");
182                let file = std::fs::File::open(prebuilt).with_context(|| {
183                    format!("failed to open prebuilt initrd at {}", prebuilt.display())
184                })?;
185                *initrd = Some(file);
186            }
187        }
188
189        let (emulated_serial_config, log_stream_tasks, linux_direct_serial_agent) =
190            if !properties.enable_serial {
191                // No emulated serial backends (OpenHCL VMBus serial stubs may still exist)
192                ([None, None, None, None], Vec::new(), None)
193            } else {
194                let SerialData {
195                    emulated_serial_config,
196                    serial_tasks,
197                    linux_direct_serial_agent,
198                } = setup.configure_serial(log_source)?;
199                (
200                    emulated_serial_config,
201                    serial_tasks,
202                    linux_direct_serial_agent,
203                )
204            };
205        let mut emulated_serial_config = emulated_serial_config;
206
207        let (video_dev, framebuffer, framebuffer_view) = match setup.config_video()? {
208            Some((v, fb, fba)) => {
209                chipset = chipset.with_framebuffer();
210                (Some(v), Some(fb), Some(fba.view()?))
211            }
212            None => (None, None, None),
213        };
214
215        if properties.no_vmbus {
216            chipset = chipset.without_vmbus();
217        }
218
219        let ide_disks = ide_controllers_to_openvmm(firmware.ide_controllers()).await?;
220        let (mut vmbus_devices, vpci_devices) =
221            vmbus_storage_controllers_to_openvmm(&vmbus_storage_controllers).await?;
222
223        let mut pcie_devices = Vec::new();
224        for PcieNvmeDrive {
225            port_name,
226            nsid,
227            drive: Drive { disk, .. },
228        } in pcie_nvme_drives
229        {
230            let disk = disk.ok_or_else(|| {
231                anyhow::anyhow!(
232                    "missing disk for PCIe NVMe drive on port '{port_name}' (nsid {nsid})"
233                )
234            })?;
235            let disk = petri_disk_to_openvmm(&disk).await?;
236            pcie_devices.push(PcieDeviceConfig {
237                port_name,
238                resource: NvmeControllerHandle {
239                    subsystem_id: Guid::new_random(),
240                    max_io_queues: 64,
241                    msix_count: 64,
242                    namespaces: vec![NamespaceDefinition {
243                        nsid,
244                        read_only: false,
245                        disk,
246                    }],
247                    requests: None,
248                }
249                .into_resource(),
250            });
251        }
252
253        let (firmware_event_send, firmware_event_recv) = mesh::mpsc_channel();
254
255        let make_vsock_listener = || -> anyhow::Result<(UnixListener, TempPath)> {
256            Ok(tempfile::Builder::new()
257                .make(|path| UnixListener::bind(path))?
258                .into_parts())
259        };
260
261        let (with_vtl2, vtl2_vmbus, ged, ged_send, vtl2_vsock_path) = if firmware.is_openhcl() {
262            let (ged, ged_send) = setup
263                .config_openhcl_vmbus_devices(
264                    &mut emulated_serial_config,
265                    &mut vmbus_devices,
266                    &firmware_event_send,
267                    framebuffer.is_some(),
268                )
269                .await?;
270
271            let late_map_vtl0_memory = match load_mode {
272                LoadMode::Igvm {
273                    vtl2_base_address: Vtl2BaseAddressType::Vtl2Allocate { .. },
274                    ..
275                } => {
276                    // Late Map VTL0 memory not supported when test supplies Vtl2Allocate
277                    None
278                }
279                _ => Some(LateMapVtl0MemoryPolicy::InjectException),
280            };
281
282            let (vtl2_vsock_listener, vtl2_vsock_path) = make_vsock_listener()?;
283            (
284                Some(Vtl2Config {
285                    vtl0_alias_map: false, // TODO: enable when OpenVMM supports it for DMA
286                    late_map_vtl0_memory,
287                }),
288                Some(VmbusConfig {
289                    vsock_listener: Some(vtl2_vsock_listener),
290                    vsock_path: Some(vtl2_vsock_path.to_string_lossy().into_owned()),
291                    vmbus_max_version: None,
292                    vtl2_redirect: false,
293                    #[cfg(windows)]
294                    vmbusproxy_handle: None,
295                }),
296                Some(ged),
297                Some(ged_send),
298                Some(vtl2_vsock_path),
299            )
300        } else {
301            (None, None, None, None, None)
302        };
303
304        // Configure the serial ports now that they have been updated by the
305        // OpenHCL configuration.
306        if properties.enable_serial {
307            chipset = chipset.with_serial(emulated_serial_config);
308            // Set so that we don't pull serial data until the guest is
309            // ready. Otherwise, Linux will drop the input serial data
310            // on the floor during boot.
311            if matches!(firmware, Firmware::LinuxDirect { .. }) && !properties.uses_pipette_as_init
312            {
313                chipset = chipset.with_serial_wait_for_rts();
314            }
315        }
316
317        // Extract video configuration
318        let vga_firmware = match video_dev {
319            Some(VideoDevice::Vga(firmware)) => Some(firmware),
320            Some(VideoDevice::Synth(vtl, resource)) => {
321                vmbus_devices.push((vtl, resource));
322                None
323            }
324            None => None,
325        };
326
327        // Add default VMBus devices (skipped in minimal mode and no-vmbus mode).
328        let (shutdown_ic_send, kvp_ic_send) = if !properties.minimal_mode && !properties.no_vmbus {
329            let (shutdown_ic_send, shutdown_ic_recv) = mesh::channel();
330            vmbus_devices.push((
331                DeviceVtl::Vtl0,
332                ShutdownIcHandle {
333                    recv: shutdown_ic_recv,
334                }
335                .into_resource(),
336            ));
337
338            let (kvp_ic_send, kvp_ic_recv) = mesh::channel();
339            vmbus_devices.push((
340                DeviceVtl::Vtl0,
341                hyperv_ic_resources::kvp::KvpIcHandle { recv: kvp_ic_recv }.into_resource(),
342            ));
343
344            vmbus_devices.push((
345                DeviceVtl::Vtl0,
346                hyperv_ic_resources::timesync::TimesyncIcHandle.into_resource(),
347            ));
348
349            (Some(shutdown_ic_send), Some(kvp_ic_send))
350        } else {
351            (None, None)
352        };
353
354        // Make a vmbus or virtio vsock path for pipette connections
355        let (vsock_listener, vsock_path) = make_vsock_listener()?;
356        let mut vsock_listener = Some(vsock_listener);
357        let vsock_path_string = vsock_path.to_string_lossy();
358
359        // Configure the UEFI helper device on the chipset for Firmware::Uefi.
360        // OpenhclUefi uses BaseChipsetType::HclHost, so it does not need this.
361        if matches!(firmware, Firmware::Uefi { .. }) {
362            let uefi_cfg = firmware.uefi_config();
363            let custom_uefi_vars =
364                uefi_cfg.map_or_else(Default::default, |c| match (arch, c.secure_boot_template) {
365                    (MachineArch::X86_64, Some(SecureBootTemplate::MicrosoftWindows)) => {
366                        hyperv_secure_boot_templates::x64::microsoft_windows()
367                    }
368                    (
369                        MachineArch::X86_64,
370                        Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority),
371                    ) => hyperv_secure_boot_templates::x64::microsoft_uefi_ca(),
372                    (MachineArch::Aarch64, Some(SecureBootTemplate::MicrosoftWindows)) => {
373                        hyperv_secure_boot_templates::aarch64::microsoft_windows()
374                    }
375                    (
376                        MachineArch::Aarch64,
377                        Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority),
378                    ) => hyperv_secure_boot_templates::aarch64::microsoft_uefi_ca(),
379                    (_, None) => Default::default(),
380                });
381            let secure_boot = uefi_cfg.is_some_and(|c| c.secure_boot_enabled);
382            let log_level = match uefi_cfg
383                .map(|c| c.efi_diagnostics_log_level)
384                .unwrap_or_default()
385            {
386                EfiDiagnosticsLogLevel::Default => {
387                    firmware_uefi_resources::LogLevel::make_default()
388                }
389                EfiDiagnosticsLogLevel::Info => firmware_uefi_resources::LogLevel::make_info(),
390                EfiDiagnosticsLogLevel::Full => firmware_uefi_resources::LogLevel::make_full(),
391            };
392            let diagnostics_rate_limit = uefi_cfg.and_then(|c| c.efi_diagnostics_rate_limit);
393            let nvram_storage = if vmgs.disk().is_some() {
394                VmgsFileHandle::new(vmgs_format::FileId::BIOS_NVRAM, true).into_resource()
395            } else {
396                EphemeralNonVolatileStoreHandle.into_resource()
397            };
398            chipset = chipset.with_uefi(vm_manifest_builder::UefiManifest::new(
399                match arch {
400                    MachineArch::X86_64 => vm_manifest_builder::MachineArch::X86_64,
401                    MachineArch::Aarch64 => vm_manifest_builder::MachineArch::Aarch64,
402                },
403                custom_uefi_vars,
404                secure_boot,
405                log_level,
406                diagnostics_rate_limit,
407                nvram_storage,
408                None,
409            ));
410        }
411
412        let layout_config = chipset.layout_config();
413        let chipset = chipset
414            .build()
415            .context("failed to build chipset configuration")?;
416
417        let numa = {
418            let MemoryConfig {
419                startup_bytes,
420                dynamic_memory_range,
421                numa_mem_sizes,
422            } = memory;
423
424            if dynamic_memory_range.is_some() {
425                anyhow::bail!("dynamic memory not supported in OpenVMM");
426            }
427
428            let make_mem = |size: u64| openvmm_defs::config::MemoryConfig {
429                mem_size: size,
430                prefetch_memory: false,
431                private_memory: false,
432                transparent_hugepages: false,
433                hugepages: false,
434                hugepage_size: None,
435                host_numa_node: None,
436            };
437
438            if let Some(sizes) = numa_mem_sizes {
439                NumaTopology {
440                    nodes: sizes
441                        .into_iter()
442                        .map(|size| NumaNode {
443                            mem: if size > 0 { Some(make_mem(size)) } else { None },
444                            vps: VpAssignment::FromTopology,
445                        })
446                        .collect(),
447                    distances: vec![],
448                }
449            } else {
450                NumaTopology {
451                    nodes: vec![NumaNode {
452                        mem: Some(make_mem(startup_bytes)),
453                        vps: VpAssignment::FromTopology,
454                    }],
455                    distances: vec![],
456                }
457            }
458        };
459
460        let processor_topology = {
461            let ProcessorTopology {
462                vp_count,
463                enable_smt,
464                vps_per_socket,
465                apic_mode,
466            } = proc_topology;
467
468            ProcessorTopologyConfig {
469                proc_count: vp_count,
470                vps_per_socket,
471                enable_smt,
472                arch: Some(match arch {
473                    MachineArch::X86_64 => openvmm_defs::config::ArchTopologyConfig::X86(
474                        openvmm_defs::config::X86TopologyConfig {
475                            x2apic: match apic_mode {
476                                None => openvmm_defs::config::X2ApicConfig::Auto,
477                                Some(x) => match x {
478                                    crate::ApicMode::Xapic => {
479                                        openvmm_defs::config::X2ApicConfig::Unsupported
480                                    }
481                                    crate::ApicMode::X2apicSupported => {
482                                        openvmm_defs::config::X2ApicConfig::Supported
483                                    }
484                                    crate::ApicMode::X2apicEnabled => {
485                                        openvmm_defs::config::X2ApicConfig::Enabled
486                                    }
487                                },
488                            },
489                            ..Default::default()
490                        },
491                    ),
492                    MachineArch::Aarch64 => openvmm_defs::config::ArchTopologyConfig::Aarch64(
493                        openvmm_defs::config::Aarch64TopologyConfig::default(),
494                    ),
495                }),
496            }
497        };
498
499        let (secure_boot_enabled, custom_uefi_vars) = firmware.uefi_config().map_or_else(
500            || (false, Default::default()),
501            |c| {
502                (
503                    c.secure_boot_enabled,
504                    match (arch, c.secure_boot_template) {
505                        (MachineArch::X86_64, Some(SecureBootTemplate::MicrosoftWindows)) => {
506                            hyperv_secure_boot_templates::x64::microsoft_windows()
507                        }
508                        (
509                            MachineArch::X86_64,
510                            Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority),
511                        ) => hyperv_secure_boot_templates::x64::microsoft_uefi_ca(),
512                        (MachineArch::Aarch64, Some(SecureBootTemplate::MicrosoftWindows)) => {
513                            hyperv_secure_boot_templates::aarch64::microsoft_windows()
514                        }
515                        (
516                            MachineArch::Aarch64,
517                            Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority),
518                        ) => hyperv_secure_boot_templates::aarch64::microsoft_uefi_ca(),
519                        (_, None) => Default::default(),
520                    },
521                )
522            },
523        );
524
525        let vmgs = if firmware.is_openhcl() {
526            None
527        } else {
528            Some(memdiff_vmgs(&vmgs).await?)
529        };
530
531        let VmChipsetResult {
532            chipset,
533            mut chipset_devices,
534            pci_chipset_devices,
535            isa_dma_controller,
536            capabilities,
537        } = chipset;
538
539        // Add the TPM
540        if let Some(tpm) = setup.config_tpm().await? {
541            chipset_devices.push(tpm);
542        }
543
544        // Set up virtio-vsock if enabled.
545        // Find the first unused PCIe root port to avoid conflicting with
546        // NVMe devices that were already assigned.
547        if properties.use_virtio_vsock {
548            let vsock_port = (0..)
549                .map(|i| format!("s0rc0rp{i}"))
550                .find(|name| !pcie_devices.iter().any(|d| d.port_name == *name))
551                .unwrap();
552            pcie_devices.push(PcieDeviceConfig {
553                port_name: vsock_port,
554                resource: VirtioPciDeviceHandle(
555                    VirtioVsockHandle {
556                        guest_cid: 0x3,
557                        base_path: vsock_path_string.to_string(),
558                        listener: vsock_listener.take().unwrap(),
559                    }
560                    .into_resource(),
561                )
562                .into_resource(),
563            });
564        }
565
566        let config = Config {
567            // Firmware
568            load_mode,
569            firmware_event_send: Some(firmware_event_send),
570
571            // CPU and RAM
572            numa,
573            processor_topology,
574
575            // Base chipset
576            chipset,
577            chipset_devices,
578            pci_chipset_devices,
579            isa_dma_controller,
580            chipset_capabilities: capabilities,
581            layout: layout_config,
582
583            // Basic virtualization device support
584            hypervisor: HypervisorConfig {
585                with_hv: true,
586                with_vtl2,
587                with_isolation: match firmware.isolation() {
588                    Some(IsolationType::Vbs) => Some(openvmm_defs::config::IsolationType::Vbs),
589                    None => None,
590                    _ => anyhow::bail!("unsupported isolation type"),
591                },
592            },
593            vmbus: if properties.no_vmbus {
594                None
595            } else {
596                Some(VmbusConfig {
597                    // If virtio vsock is enabled, the vsock_listener will have already been taken
598                    // and is now None.
599                    vsock_listener,
600                    vsock_path: (!properties.use_virtio_vsock)
601                        .then(|| vsock_path_string.to_string()),
602                    vmbus_max_version: None,
603                    vtl2_redirect: firmware.openhcl_config().is_some_and(|c| c.vmbus_redirect),
604                    #[cfg(windows)]
605                    vmbusproxy_handle: None,
606                })
607            },
608            vtl2_vmbus,
609
610            // Devices
611            floppy_disks: vec![],
612            ide_disks,
613            pcie_root_complexes: vec![],
614            pcie_devices,
615            pcie_switches: vec![],
616            pcie_generic_initiators: vec![],
617            vpci_devices,
618            vmbus_devices,
619
620            // Video support
621            framebuffer,
622            vga_firmware,
623
624            secure_boot_enabled,
625            custom_uefi_vars,
626            vmgs,
627
628            // Don't automatically reset the guest by default
629            automatic_guest_reset: false,
630
631            // Disabled for VMM tests by default
632            #[cfg(windows)]
633            kernel_vmnics: vec![],
634            input: mesh::Receiver::new(),
635            vtl2_gfx: false,
636            virtio_devices: vec![],
637            #[cfg(windows)]
638            vpci_resources: vec![],
639            debugger_rpc: None,
640            rtc_delta_milliseconds: 0,
641            efi_diagnostics_log_level: match firmware
642                .uefi_config()
643                .map(|c| c.efi_diagnostics_log_level)
644                .unwrap_or_default()
645            {
646                EfiDiagnosticsLogLevel::Default => {
647                    openvmm_defs::config::EfiDiagnosticsLogLevelType::Default
648                }
649                EfiDiagnosticsLogLevel::Info => {
650                    openvmm_defs::config::EfiDiagnosticsLogLevelType::Info
651                }
652                EfiDiagnosticsLogLevel::Full => {
653                    openvmm_defs::config::EfiDiagnosticsLogLevelType::Full
654                }
655            },
656        };
657
658        // Make the pipette connection listener.
659        let path = format!("{vsock_path_string}_{PIPETTE_PORT}");
660        let pipette_listener = PolledSocket::new(
661            driver,
662            UnixListener::bind(path).context("failed to bind to pipette listener")?,
663        )?;
664
665        // Make the vtl2 pipette connection listener.
666        let vtl2_pipette_listener = if let Some(vtl2_vmbus) = &config.vtl2_vmbus {
667            let path = vtl2_vmbus.vsock_path.as_ref().unwrap();
668            let path = format!("{path}_{PIPETTE_PORT}");
669            Some(PolledSocket::new(
670                driver,
671                UnixListener::bind(path).context("failed to bind to vtl2 pipette listener")?,
672            )?)
673        } else {
674            None
675        };
676
677        Ok(Self {
678            runtime_config: firmware.into_runtime_config(vmbus_storage_controllers),
679            arch,
680            host_log_levels,
681            config,
682            mesh,
683
684            resources: PetriVmResourcesOpenVmm {
685                log_stream_tasks,
686                firmware_event_recv,
687                shutdown_ic_send,
688                kvp_ic_send,
689                ged_send,
690                pipette_listener,
691                vtl2_pipette_listener,
692                linux_direct_serial_agent,
693                tcp_pipette_port: None,
694                driver: driver.clone(),
695                output_dir: log_source.output_dir().to_owned(),
696                openvmm_path: openvmm_path.clone(),
697                vtl2_vsock_path,
698                _vsock_path: vsock_path,
699                properties,
700                #[cfg(windows)]
701                _switch_ports: Vec::new(),
702            },
703
704            openvmm_log_file: log_source.log_file("openvmm")?,
705
706            memory_backing_file: None,
707
708            ged,
709            framebuffer_view,
710
711            pending_iommu: Vec::new(),
712        })
713    }
714}
715
716struct PetriVmConfigSetupCore<'a> {
717    arch: MachineArch,
718    firmware: &'a Firmware,
719    driver: &'a DefaultDriver,
720    logger: &'a PetriLogSource,
721    vmgs: &'a PetriVmgsResource,
722    tpm_config: Option<&'a TpmConfig>,
723    mesh: &'a Mesh,
724    openvmm_path: &'a ResolvedArtifact,
725    uses_pipette_as_init: bool,
726    enable_serial: bool,
727    use_virtio_vsock: bool,
728    no_vmbus: bool,
729}
730
731struct SerialData {
732    emulated_serial_config: [Option<Resource<SerialBackendHandle>>; 4],
733    serial_tasks: Vec<Task<anyhow::Result<()>>>,
734    linux_direct_serial_agent: Option<LinuxDirectSerialAgent>,
735}
736
737enum VideoDevice {
738    Vga(RomFileLocation),
739    Synth(DeviceVtl, Resource<VmbusDeviceHandleKind>),
740}
741
742impl PetriVmConfigSetupCore<'_> {
743    fn configure_serial(&self, logger: &PetriLogSource) -> anyhow::Result<SerialData> {
744        let mut serial_tasks = Vec::new();
745
746        let serial0_log_file = logger.log_file(match self.firmware {
747            Firmware::LinuxDirect { .. } | Firmware::OpenhclLinuxDirect { .. } => "linux",
748            Firmware::Pcat { .. } | Firmware::OpenhclPcat { .. } => "pcat",
749            Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } => "uefi",
750        })?;
751
752        let (serial0_host, serial0) = self
753            .create_serial_stream()
754            .context("failed to create serial0 stream")?;
755        let (serial0_read, serial0_write) = serial0_host.split();
756        let serial0_task = self.driver.spawn(
757            "serial0-console",
758            crate::log_task(serial0_log_file, serial0_read, "serial0-console"),
759        );
760        serial_tasks.push(serial0_task);
761
762        let serial2 = if self.firmware.is_openhcl() {
763            let (serial2_host, serial2) = self
764                .create_serial_stream()
765                .context("failed to create serial2 stream")?;
766            let serial2_task = self.driver.spawn(
767                "serial2-openhcl",
768                crate::log_task(logger.log_file("openhcl")?, serial2_host, "serial2-openhcl"),
769            );
770            serial_tasks.push(serial2_task);
771            serial2
772        } else {
773            None
774        };
775
776        if self.firmware.is_linux_direct() && !self.uses_pipette_as_init {
777            // Non-pipette-as-init Linux direct: create serial1 and a serial
778            // agent so we can send shell commands to launch pipette.
779            let (serial1_host, serial1) = self.create_serial_stream()?;
780            let (serial1_read, _serial1_write) = serial1_host.split();
781            let linux_direct_serial_agent =
782                LinuxDirectSerialAgent::new(serial1_read, serial0_write);
783            Ok(SerialData {
784                emulated_serial_config: [serial0, serial1, serial2, None],
785                serial_tasks,
786                linux_direct_serial_agent: Some(linux_direct_serial_agent),
787            })
788        } else {
789            Ok(SerialData {
790                emulated_serial_config: [serial0, None, serial2, None],
791                serial_tasks,
792                linux_direct_serial_agent: None,
793            })
794        }
795    }
796
797    fn create_serial_stream(
798        &self,
799    ) -> anyhow::Result<(
800        PolledSocket<UnixStream>,
801        Option<Resource<SerialBackendHandle>>,
802    )> {
803        let (host_side, guest_side) = UnixStream::pair()?;
804        let host_side = PolledSocket::new(self.driver, host_side)?;
805        let serial = OpenSocketSerialConfig::from(guest_side).into_resource();
806        Ok((host_side, Some(serial)))
807    }
808
809    fn load_firmware(&self) -> anyhow::Result<LoadMode> {
810        // The test kernel has both CONFIG_VIRTIO_VSOCK=y and
811        // CONFIG_HYPERV_VSOCKETS=y built in. The kernel only allows one G2H
812        // vsock transport, and virtio_vsock_init runs first, claiming the
813        // slot. This causes hv_sock registration to fail with -EBUSY,
814        // breaking pipette's AF_VSOCK connection. Blacklist either
815        // virtio_vsock_init or hv_sock_init depending on which vsock transport
816        // is being used.
817        const VIRTIO_VSOCK_BLACKLIST: &str = "initcall_blacklist=virtio_vsock_init";
818        let vsock_blacklist = if self.use_virtio_vsock {
819            "initcall_blacklist=hv_sock_init"
820        } else {
821            VIRTIO_VSOCK_BLACKLIST
822        };
823
824        Ok(match (self.arch, &self.firmware) {
825            (arch, Firmware::LinuxDirect { kernel, initrd }) => {
826                let console = match arch {
827                    MachineArch::X86_64 => "console=ttyS0",
828                    MachineArch::Aarch64 => "console=ttyAMA0 earlycon",
829                };
830                let kernel = File::open(kernel.clone())
831                    .context("Failed to open kernel")?
832                    .into();
833                let initrd = File::open(initrd.clone())
834                    .context("Failed to open initrd")?
835                    .into();
836
837                let init = if self.uses_pipette_as_init {
838                    "/pipette"
839                } else {
840                    "/bin/sh"
841                };
842
843                let serial_args = if self.enable_serial {
844                    format!("{console} debug ")
845                } else {
846                    String::new()
847                };
848
849                let cmdline = format!("{serial_args}panic=-1 rdinit={init} {vsock_blacklist}");
850
851                LoadMode::Linux {
852                    kernel,
853                    initrd: Some(initrd),
854                    cmdline,
855                    custom_dsdt: None,
856                    enable_serial: self.enable_serial,
857                    boot_mode: openvmm_defs::config::LinuxDirectBootMode::Acpi,
858                }
859            }
860            (
861                MachineArch::X86_64,
862                Firmware::Pcat {
863                    bios_firmware: firmware,
864                    guest: _,         // load_boot_disk
865                    svga_firmware: _, // config_video
866                    ide_controllers: _,
867                },
868            ) => {
869                let firmware = openvmm_pcat_locator::find_pcat_bios(firmware.get())
870                    .context("Failed to load packaged PCAT binary")?;
871                LoadMode::Pcat {
872                    firmware,
873                    boot_order: DEFAULT_PCAT_BOOT_ORDER,
874                }
875            }
876            (
877                _,
878                Firmware::Uefi {
879                    uefi_firmware: firmware,
880                    guest: _, // load_boot_disk
881                    uefi_config:
882                        UefiConfig {
883                            secure_boot_enabled: _,  // new
884                            secure_boot_template: _, // new
885                            disable_frontpage,
886                            default_boot_always_attempt,
887                            enable_vpci_boot,
888                            force_dma_bounce,
889                            efi_diagnostics_log_level: _, // applied to top-level Config below
890                            efi_diagnostics_rate_limit: _, // applied to top-level Config below
891                        },
892                },
893            ) => {
894                let firmware = File::open(firmware.clone())
895                    .context("Failed to open uefi firmware file")?
896                    .into();
897                LoadMode::Uefi {
898                    firmware,
899                    enable_debugging: false,
900                    enable_memory_protections: false,
901                    disable_frontpage: *disable_frontpage,
902                    enable_tpm: self.tpm_config.is_some(),
903                    enable_battery: false,
904                    enable_serial: true,
905                    enable_vpci_boot: *enable_vpci_boot,
906                    uefi_console_mode: Some(openvmm_defs::config::UefiConsoleMode::Com1),
907                    default_boot_always_attempt: *default_boot_always_attempt,
908                    bios_guid: Guid::new_random(),
909                    enable_vmbus: !self.no_vmbus,
910                    force_dma_bounce: *force_dma_bounce,
911                }
912            }
913            (
914                MachineArch::X86_64,
915                Firmware::OpenhclLinuxDirect {
916                    igvm_path,
917                    openhcl_config,
918                }
919                | Firmware::OpenhclUefi {
920                    igvm_path,
921                    guest: _,       // load_boot_disk
922                    isolation: _,   // new via Firmware::isolation
923                    uefi_config: _, // config_openhcl_vmbus_devices
924                    openhcl_config,
925                },
926            ) => {
927                let OpenHclConfig {
928                    vmbus_redirect: _, // config_openhcl_vmbus_devices
929                    custom_command_line: _,
930                    log_levels: _,
931                    vtl2_base_address_type,
932                    vtl2_settings: _, // run_core
933                } = openhcl_config;
934
935                let mut cmdline = Some(openhcl_config.command_line());
936
937                append_cmdline(&mut cmdline, "panic=-1 reboot=triple");
938
939                let isolated = match self.firmware {
940                    Firmware::OpenhclLinuxDirect { .. } => {
941                        // Set UNDERHILL_SERIAL_WAIT_FOR_RTS=1 so that we don't pull serial data
942                        // until the guest is ready. Otherwise, Linux will drop the input serial
943                        // data on the floor during boot.
944                        append_cmdline(
945                            &mut cmdline,
946                            format!(
947                                "UNDERHILL_SERIAL_WAIT_FOR_RTS=1 UNDERHILL_CMDLINE_APPEND=\"rdinit=/bin/sh {vsock_blacklist}\""
948                            ),
949                        );
950                        false
951                    }
952                    Firmware::OpenhclUefi { isolation, .. } if isolation.is_some() => true,
953                    _ => false,
954                };
955
956                // For certain configurations, we need to override the override
957                // in new_underhill_vm.
958                //
959                // TODO: remove this (and OpenHCL override) once host changes
960                // are saturated.
961                if let Firmware::OpenhclUefi {
962                    uefi_config:
963                        UefiConfig {
964                            default_boot_always_attempt,
965                            secure_boot_enabled,
966                            ..
967                        },
968                    ..
969                } = self.firmware
970                {
971                    if !isolated
972                        && !secure_boot_enabled
973                        && self.tpm_config.is_none()
974                        && !default_boot_always_attempt
975                    {
976                        append_cmdline(&mut cmdline, "HCL_DEFAULT_BOOT_ALWAYS_ATTEMPT=0");
977                    }
978                }
979
980                // Plumb the EFI diagnostics rate-limit override to the
981                // OpenHCL-side UEFI device via env var.
982                if let Firmware::OpenhclUefi {
983                    uefi_config:
984                        UefiConfig {
985                            efi_diagnostics_rate_limit: Some(limit),
986                            ..
987                        },
988                    ..
989                } = self.firmware
990                {
991                    append_cmdline(
992                        &mut cmdline,
993                        format!("HCL_EFI_DIAGNOSTICS_RATE_LIMIT={limit}"),
994                    );
995                }
996
997                let vtl2_base_address = vtl2_base_address_type.unwrap_or_else(|| {
998                    if isolated {
999                        // Isolated VMs must load at the location specified by
1000                        // the file, as they do not support relocation.
1001                        Vtl2BaseAddressType::File
1002                    } else {
1003                        // By default, utilize IGVM relocation and tell OpenVMM
1004                        // to place VTL2 at 512MB. This tests both relocation
1005                        // support in OpenVMM, and relocation support within
1006                        // OpenHCL.
1007                        Vtl2BaseAddressType::Absolute(512 * SIZE_1_MB)
1008                    }
1009                });
1010
1011                let file = File::open(igvm_path.clone())
1012                    .context("failed to open openhcl firmware file")?
1013                    .into();
1014                LoadMode::Igvm {
1015                    file,
1016                    cmdline: cmdline.unwrap_or_default(),
1017                    vtl2_base_address,
1018                    com_serial: Some(SerialInformation {
1019                        io_port: ComPort::Com3.io_port(),
1020                        irq: ComPort::Com3.irq().into(),
1021                    }),
1022                }
1023            }
1024            (a, f) => anyhow::bail!("Unsupported firmware {f:?} for arch {a:?}"),
1025        })
1026    }
1027
1028    async fn config_openhcl_vmbus_devices(
1029        &self,
1030        serial: &mut [Option<Resource<SerialBackendHandle>>],
1031        devices: &mut impl Extend<(DeviceVtl, Resource<VmbusDeviceHandleKind>)>,
1032        firmware_event_send: &mesh::Sender<FirmwareEvent>,
1033        framebuffer: bool,
1034    ) -> anyhow::Result<(
1035        get_resources::ged::GuestEmulationDeviceHandle,
1036        mesh::Sender<get_resources::ged::GuestEmulationRequest>,
1037    )> {
1038        let serial0 = serial[0].take();
1039        devices.extend([(
1040            DeviceVtl::Vtl2,
1041            VmbusSerialDeviceHandle {
1042                port: VmbusSerialPort::Com1,
1043                backend: serial0.unwrap_or_else(|| DisconnectedSerialBackendHandle.into_resource()),
1044            }
1045            .into_resource(),
1046        )]);
1047        let serial1 = serial[1].take();
1048        devices.extend([(
1049            DeviceVtl::Vtl2,
1050            VmbusSerialDeviceHandle {
1051                port: VmbusSerialPort::Com2,
1052                backend: serial1.unwrap_or_else(|| DisconnectedSerialBackendHandle.into_resource()),
1053            }
1054            .into_resource(),
1055        )]);
1056
1057        let crash = spawn_dump_handler(self.driver, self.logger).into_resource();
1058        devices.extend([(DeviceVtl::Vtl2, crash)]);
1059
1060        let (guest_request_send, guest_request_recv) = mesh::channel();
1061
1062        let (
1063            UefiConfig {
1064                secure_boot_enabled,
1065                secure_boot_template,
1066                disable_frontpage,
1067                default_boot_always_attempt,
1068                enable_vpci_boot,
1069                force_dma_bounce,
1070                efi_diagnostics_log_level,
1071                efi_diagnostics_rate_limit: _, // applied device-side via UefiManifest::new
1072            },
1073            OpenHclConfig { vmbus_redirect, .. },
1074        ) = match self.firmware {
1075            Firmware::OpenhclUefi {
1076                uefi_config,
1077                openhcl_config,
1078                ..
1079            } => (uefi_config, openhcl_config),
1080            Firmware::OpenhclLinuxDirect { openhcl_config, .. } => {
1081                (&UefiConfig::default(), openhcl_config)
1082            }
1083            _ => anyhow::bail!("not a supported openhcl firmware config"),
1084        };
1085
1086        let test_gsp_by_id = matches!(
1087            self.vmgs.encryption_policy(),
1088            Some(GuestStateEncryptionPolicy::GspById(_))
1089        );
1090
1091        // Save the GED handle to add later after configuration is complete.
1092        let ged = get_resources::ged::GuestEmulationDeviceHandle {
1093            firmware: get_resources::ged::GuestFirmwareConfig::Uefi {
1094                firmware_debug: false,
1095                disable_frontpage: *disable_frontpage,
1096                enable_vpci_boot: *enable_vpci_boot,
1097                console_mode: get_resources::ged::UefiConsoleMode::COM1,
1098                default_boot_always_attempt: *default_boot_always_attempt,
1099            },
1100            com1: true,
1101            com2: true,
1102            serial_tx_only: false,
1103            vmbus_redirection: *vmbus_redirect,
1104            vtl2_settings: None, // Will be added at startup to allow tests to modify
1105            vmgs: memdiff_vmgs(self.vmgs).await?,
1106            framebuffer: framebuffer.then(|| SharedFramebufferHandle.into_resource()),
1107            guest_request_recv,
1108            enable_tpm: self.tpm_config.is_some(),
1109            firmware_event_send: Some(firmware_event_send.clone()),
1110            secure_boot_enabled: *secure_boot_enabled,
1111            secure_boot_template: match secure_boot_template {
1112                Some(SecureBootTemplate::MicrosoftWindows) => {
1113                    get_resources::ged::GuestSecureBootTemplateType::MicrosoftWindows
1114                }
1115                Some(SecureBootTemplate::MicrosoftUefiCertificateAuthority) => {
1116                    get_resources::ged::GuestSecureBootTemplateType::MicrosoftUefiCertificateAuthority
1117                }
1118                None => get_resources::ged::GuestSecureBootTemplateType::None,
1119            },
1120            enable_battery: false,
1121            no_persistent_secrets: self.tpm_config.as_ref().is_some_and(|c| c.no_persistent_secrets),
1122            igvm_attest_test_config: None,
1123            test_gsp_by_id,
1124            efi_diagnostics_log_level: match efi_diagnostics_log_level {
1125                EfiDiagnosticsLogLevel::Default => {
1126                    get_resources::ged::EfiDiagnosticsLogLevelType::Default
1127                }
1128                EfiDiagnosticsLogLevel::Info => {
1129                    get_resources::ged::EfiDiagnosticsLogLevelType::Info
1130                }
1131                EfiDiagnosticsLogLevel::Full => {
1132                    get_resources::ged::EfiDiagnosticsLogLevelType::Full
1133                }
1134            },
1135            force_dma_bounce_enabled: *force_dma_bounce,
1136        };
1137
1138        Ok((ged, guest_request_send))
1139    }
1140
1141    fn config_video(
1142        &self,
1143    ) -> anyhow::Result<Option<(VideoDevice, Framebuffer, FramebufferAccess)>> {
1144        if self.firmware.isolation().is_some() {
1145            return Ok(None);
1146        }
1147
1148        let video_dev = match self.firmware {
1149            Firmware::Pcat { svga_firmware, .. } | Firmware::OpenhclPcat { svga_firmware, .. } => {
1150                Some(VideoDevice::Vga(
1151                    openvmm_pcat_locator::find_svga_bios(svga_firmware.get())
1152                        .context("Failed to load VGA BIOS")?,
1153                ))
1154            }
1155            Firmware::Uefi { .. } | Firmware::OpenhclUefi { .. } if !self.no_vmbus => {
1156                Some(VideoDevice::Synth(
1157                    DeviceVtl::Vtl0,
1158                    SynthVideoHandle {
1159                        framebuffer: SharedFramebufferHandle.into_resource(),
1160                        dirt_send: None,
1161                    }
1162                    .into_resource(),
1163                ))
1164            }
1165            Firmware::OpenhclLinuxDirect { .. }
1166            | Firmware::LinuxDirect { .. }
1167            | Firmware::Uefi { .. }
1168            | Firmware::OpenhclUefi { .. } => None,
1169        };
1170
1171        Ok(if let Some(vdev) = video_dev {
1172            let vram =
1173                alloc_shared_memory(FRAMEBUFFER_SIZE, "vram").context("allocating framebuffer")?;
1174            let (fb, fba) = framebuffer::framebuffer(vram, FRAMEBUFFER_SIZE, 0)
1175                .context("creating framebuffer")?;
1176            Some((vdev, fb, fba))
1177        } else {
1178            None
1179        })
1180    }
1181
1182    async fn config_tpm(&self) -> anyhow::Result<Option<ChipsetDeviceHandle>> {
1183        if !self.firmware.is_openhcl()
1184            && let Some(TpmConfig {
1185                no_persistent_secrets,
1186            }) = self.tpm_config
1187        {
1188            let register_layout = match self.arch {
1189                MachineArch::X86_64 => TpmRegisterLayout::IoPort,
1190                MachineArch::Aarch64 => TpmRegisterLayout::Mmio,
1191            };
1192
1193            let (ppi_store, nvram_store) = if self.vmgs.disk().is_none() || *no_persistent_secrets {
1194                (
1195                    EphemeralNonVolatileStoreHandle.into_resource(),
1196                    EphemeralNonVolatileStoreHandle.into_resource(),
1197                )
1198            } else {
1199                (
1200                    VmgsFileHandle::new(vmgs_format::FileId::TPM_PPI, true).into_resource(),
1201                    VmgsFileHandle::new(vmgs_format::FileId::TPM_NVRAM, true).into_resource(),
1202                )
1203            };
1204
1205            Ok(Some(ChipsetDeviceHandle {
1206                name: "tpm".to_string(),
1207                resource: chipset_device_worker_defs::RemoteChipsetDeviceHandle {
1208                    device: TpmDeviceHandle {
1209                        ppi_store,
1210                        nvram_store,
1211                        refresh_tpm_seeds: false,
1212                        ak_cert_type: tpm_resources::TpmAkCertTypeResource::None,
1213                        register_layout,
1214                        guest_secret_key: None,
1215                        logger: None,
1216                        is_confidential_vm: self.firmware.isolation().is_some(),
1217                        // TODO: generate an actual BIOS GUID and put it here
1218                        bios_guid: Guid::ZERO,
1219                        nvram_size: None,
1220                    }
1221                    .into_resource(),
1222                    worker_host: self.make_device_worker("tpm").await?,
1223                }
1224                .into_resource(),
1225            }))
1226        } else {
1227            Ok(None)
1228        }
1229    }
1230
1231    async fn make_device_worker(&self, name: &str) -> anyhow::Result<mesh_worker::WorkerHost> {
1232        let (host, runner) = mesh_worker::worker_host();
1233        self.mesh
1234            .launch_host(
1235                mesh_process::ProcessConfig::new(name).process_name(self.openvmm_path),
1236                openvmm_defs::entrypoint::MeshHostParams { runner },
1237            )
1238            .await?;
1239        Ok(host)
1240    }
1241}
1242
1243fn spawn_dump_handler(driver: &DefaultDriver, logger: &PetriLogSource) -> GuestCrashDeviceHandle {
1244    let (send, mut recv) = mesh::channel();
1245    let handle = GuestCrashDeviceHandle {
1246        request_dump: send,
1247        max_dump_size: 256 * 1024 * 1024,
1248    };
1249    driver
1250        .spawn("openhcl-dump-handler", {
1251            let logger = logger.clone();
1252            let driver = driver.clone();
1253            async move {
1254                while let Some(rpc) = recv.next().await {
1255                    rpc.handle_failable_sync(|done| {
1256                        let (file, path) = logger.create_attachment("openhcl.core")?.into_parts();
1257                        driver
1258                            .spawn("crash-waiter", async move {
1259                                let filename = path.file_name().unwrap().to_str().unwrap();
1260                                if done.await.is_ok() {
1261                                    tracing::warn!(filename, "openhcl crash dump complete");
1262                                } else {
1263                                    tracing::error!(
1264                                        filename,
1265                                        "openhcl crash dump incomplete, may be corrupted"
1266                                    );
1267                                }
1268                            })
1269                            .detach();
1270                        anyhow::Ok(file)
1271                    })
1272                }
1273            }
1274        })
1275        .detach();
1276    handle
1277}
1278
1279/// Convert the generic IDE configuration to OpenVMM IDE disks.
1280async fn ide_controllers_to_openvmm(
1281    ide_controllers: Option<&[[Option<Drive>; 2]; 2]>,
1282) -> anyhow::Result<Vec<IdeDeviceConfig>> {
1283    let mut ide_disks = Vec::new();
1284
1285    if let Some(ide_controllers) = ide_controllers {
1286        for (controller_number, controller) in ide_controllers.iter().enumerate() {
1287            for (controller_location, drive) in controller.iter().enumerate() {
1288                if let Some(drive) = drive {
1289                    if let Some(disk) = &drive.disk {
1290                        let disk = petri_disk_to_openvmm(disk).await?;
1291                        let guest_media = if drive.is_dvd {
1292                            GuestMedia::Dvd(
1293                                SimpleScsiDvdHandle {
1294                                    media: Some(disk),
1295                                    requests: None,
1296                                }
1297                                .into_resource(),
1298                            )
1299                        } else {
1300                            GuestMedia::Disk {
1301                                disk_type: disk,
1302                                read_only: false,
1303                                disk_parameters: None,
1304                            }
1305                        };
1306
1307                        ide_disks.push(IdeDeviceConfig {
1308                            path: ide_resources::IdePath {
1309                                channel: controller_number as u8,
1310                                drive: controller_location as u8,
1311                            },
1312                            guest_media,
1313                        });
1314                    }
1315                }
1316            }
1317        }
1318    }
1319
1320    Ok(ide_disks)
1321}
1322
1323/// Convert the generic VMBUS storage configuration to OpenVMM VMBUS and VPCI devices.
1324async fn vmbus_storage_controllers_to_openvmm(
1325    vmbus_storage_controllers: &HashMap<Guid, VmbusStorageController>,
1326) -> anyhow::Result<(
1327    Vec<(DeviceVtl, Resource<VmbusDeviceHandleKind>)>,
1328    Vec<VpciDeviceConfig>,
1329)> {
1330    let mut vmbus_devices = Vec::new();
1331    let mut vpci_devices = Vec::new();
1332
1333    // Add VMBus storage
1334    for (instance_id, controller) in vmbus_storage_controllers {
1335        let vtl = match controller.target_vtl {
1336            crate::Vtl::Vtl0 => DeviceVtl::Vtl0,
1337            crate::Vtl::Vtl1 => DeviceVtl::Vtl1,
1338            crate::Vtl::Vtl2 => DeviceVtl::Vtl2,
1339        };
1340        match controller.controller_type {
1341            VmbusStorageType::Scsi => {
1342                let mut devices = Vec::new();
1343                for (lun, Drive { disk, is_dvd }) in &controller.drives {
1344                    if !*is_dvd && let Some(disk) = disk {
1345                        devices.push(ScsiDeviceAndPath {
1346                            path: ScsiPath {
1347                                path: 0,
1348                                target: 0,
1349                                lun: (*lun).try_into().expect("invalid scsi lun"),
1350                            },
1351                            device: SimpleScsiDiskHandle {
1352                                disk: petri_disk_to_openvmm(disk).await?,
1353                                read_only: false,
1354                                parameters: Default::default(),
1355                            }
1356                            .into_resource(),
1357                        });
1358                    } else {
1359                        todo!("dvd ({}) or empty ({})", *is_dvd, disk.is_none())
1360                    }
1361                }
1362
1363                vmbus_devices.push((
1364                    vtl,
1365                    ScsiControllerHandle {
1366                        instance_id: *instance_id,
1367                        max_sub_channel_count: 1,
1368                        io_queue_depth: None,
1369                        devices,
1370                        requests: None,
1371                        poll_mode_queue_depth: None,
1372                    }
1373                    .into_resource(),
1374                ));
1375            }
1376            VmbusStorageType::Nvme => {
1377                let mut namespaces = Vec::new();
1378                for (nsid, Drive { disk, is_dvd }) in &controller.drives {
1379                    if !*is_dvd && let Some(disk) = disk {
1380                        namespaces.push(NamespaceDefinition {
1381                            nsid: *nsid,
1382                            read_only: false,
1383                            disk: petri_disk_to_openvmm(disk).await?,
1384                        });
1385                    } else {
1386                        todo!("dvd ({}) or empty ({})", *is_dvd, disk.is_none())
1387                    }
1388                }
1389
1390                vpci_devices.push(VpciDeviceConfig {
1391                    vtl,
1392                    instance_id: *instance_id,
1393                    resource: NvmeControllerHandle {
1394                        subsystem_id: *instance_id,
1395                        max_io_queues: 64,
1396                        msix_count: 64,
1397                        namespaces,
1398                        requests: None,
1399                    }
1400                    .into_resource(),
1401                    vnode: None,
1402                });
1403            }
1404            VmbusStorageType::VirtioBlk => {
1405                // Each virtio-blk drive needs a unique VPCI instance ID.
1406                // Use a fixed template GUID with data1 set to the LUN.
1407                const VIRTIO_BLK_INSTANCE_ID_TEMPLATE: Guid = Guid {
1408                    data1: 0,
1409                    data2: 0x1234,
1410                    data3: 0x5678,
1411                    data4: [0xab, 0xcd, 0xef, 0x01, 0x23, 0x45, 0x67, 0x89],
1412                };
1413                for (lun, Drive { disk, is_dvd }) in &controller.drives {
1414                    if *is_dvd {
1415                        anyhow::bail!("dvd not supported with virtio-blk");
1416                    }
1417                    let Some(disk) = disk else {
1418                        anyhow::bail!("empty drive not supported with virtio-blk");
1419                    };
1420                    let mut drive_id = VIRTIO_BLK_INSTANCE_ID_TEMPLATE;
1421                    drive_id.data1 = *lun;
1422                    vpci_devices.push(VpciDeviceConfig {
1423                        vtl,
1424                        instance_id: drive_id,
1425                        resource: VirtioPciDeviceHandle(
1426                            VirtioBlkHandle {
1427                                disk: petri_disk_to_openvmm(disk).await?,
1428                                read_only: false,
1429                            }
1430                            .into_resource(),
1431                        )
1432                        .into_resource(),
1433                        vnode: None,
1434                    });
1435                }
1436            }
1437        }
1438    }
1439
1440    Ok((vmbus_devices, vpci_devices))
1441}