Conversation
There was a problem hiding this comment.
Pull request overview
Adds Petri/Hyper-V support for attaching an NVMe emulator-backed disk and relaying it to the VTL0 guest via OpenHCL storvsp, along with an (unstable) x86_64 storage test that exercises the flow.
Changes:
- Add an unstable x86_64 storage test that uses an NVMe emulator-backed VHD and validates guest I/O via storvsp.
- Extend Petri VM config/builder to carry NVMe-emulator-to-SCSI LUN mapping intent.
- Add Hyper-V backend + PowerShell plumbing to attach NVMe emulator devices post-VM-creation and patch VTL2 settings with resolved VSIDs + injected LUNs.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| vmm_tests/vmm_tests/tests/tests/x86_64/storage.rs | Adds an unstable Hyper-V OpenHCL UEFI test covering NVMe-emulator-backed storage relayed via storvsp. |
| petri/src/vm/openvmm/construct.rs | Ignores the new NVMe emulator mapping field for OpenVMM backend construction (Hyper-V only). |
| petri/src/vm/mod.rs | Adds config field + builder API for NVMe emulator LUN mappings and a post-creation runtime config hook. |
| petri/src/vm/hyperv/powershell.rs | Introduces env-var driven script discovery + PowerShell invocation to attach NVMe emulator devices. |
| petri/src/vm/hyperv/mod.rs | Implements Hyper-V backend NVMe emulator attachment, VSID reconciliation, and VTL2 settings injection. |
| crate::VmbusStorageType::Nvme => { | ||
| let mut vhd_paths = Vec::new(); | ||
| for Drive { disk, is_dvd: _ } in controller.drives.values() { | ||
| let vhd = petri_disk_to_hyperv(disk.as_ref(), &temp_dir).await?; | ||
| if let Some(path) = vhd { | ||
| vhd_paths.push(path); | ||
| } | ||
| } | ||
| let vtl_num = match controller.target_vtl { | ||
| crate::Vtl::Vtl0 => 0u8, | ||
| crate::Vtl::Vtl2 => 2u8, | ||
| _ => { | ||
| anyhow::bail!( | ||
| "unsupported VTL {:?} for NVMe emulator device", | ||
| controller.target_vtl | ||
| ) | ||
| } | ||
| }; | ||
| nvme_emulator_attachments.push((*vsid, controller.clone(), vhd_paths, vtl_num)); | ||
| } |
There was a problem hiding this comment.
controller.drives is a HashMap, so iterating values() yields a nondeterministic ordering. If the NVMe emulator script assigns namespace IDs based on the order of VhdPaths, this can attach the wrong VHD to a given NSID (especially when multiple namespaces are configured). Build vhd_paths in a deterministic order based on the drive key (NSID), and validate that each entry is a non-DVD drive with a backing disk (otherwise error out instead of silently skipping).
| .unwrap_or_else(|| { | ||
| tracing::warn!( | ||
| nvme_vsid = %mapping.nvme_vsid, | ||
| "NVMe emulator LUN mapping references unknown VSID; \ | ||
| using placeholder (this will likely fail)" | ||
| ); | ||
| mapping.nvme_vsid | ||
| }); |
There was a problem hiding this comment.
When an NvmeEmulatorLunMapping references a VSID that wasn't attached, the code logs a warning and continues with the placeholder VSID, which will likely fail later in a harder-to-debug way. Consider failing early (e.g., anyhow::bail!/ensure!) when the mapping references an unknown placeholder VSID.
| .unwrap_or_else(|| { | |
| tracing::warn!( | |
| nvme_vsid = %mapping.nvme_vsid, | |
| "NVMe emulator LUN mapping references unknown VSID; \ | |
| using placeholder (this will likely fail)" | |
| ); | |
| mapping.nvme_vsid | |
| }); | |
| .ok_or_else(|| { | |
| anyhow::anyhow!( | |
| "NVMe emulator LUN mapping references unknown placeholder VSID {} \ | |
| for controller {} LUN {} namespace {}", | |
| mapping.nvme_vsid, | |
| mapping.scsi_controller_instance_id, | |
| mapping.scsi_lun, | |
| mapping.nvme_namespace_id, | |
| ) | |
| })?; |
| controller.luns.push( | ||
| Vtl2LunBuilder::disk() | ||
| .with_location(scsi_lun) | ||
| .with_physical_device(Vtl2StorageBackingDeviceBuilder::new( | ||
| ControllerType::Nvme, | ||
| actual_vsid, | ||
| nvme_nsid, | ||
| )) | ||
| .build(), | ||
| ); |
There was a problem hiding this comment.
inject_nvme_emulator_lun always appends a new LUN. If the target SCSI controller already has a LUN with the same location, this will create duplicates and can lead to confusing behavior. Consider checking for an existing LUN at scsi_lun and either replacing it or returning an error.
| controller.luns.push( | |
| Vtl2LunBuilder::disk() | |
| .with_location(scsi_lun) | |
| .with_physical_device(Vtl2StorageBackingDeviceBuilder::new( | |
| ControllerType::Nvme, | |
| actual_vsid, | |
| nvme_nsid, | |
| )) | |
| .build(), | |
| ); | |
| let lun = Vtl2LunBuilder::disk() | |
| .with_location(scsi_lun) | |
| .with_physical_device(Vtl2StorageBackingDeviceBuilder::new( | |
| ControllerType::Nvme, | |
| actual_vsid, | |
| nvme_nsid, | |
| )) | |
| .build(); | |
| if let Some(existing_lun) = controller.luns.iter_mut().find(|lun| lun.location == scsi_lun) { | |
| tracing::warn!( | |
| %scsi_controller_id, | |
| scsi_lun, | |
| "replacing existing VTL2 SCSI LUN while injecting NVMe emulator LUN" | |
| ); | |
| *existing_lun = lun; | |
| } else { | |
| controller.luns.push(lun); | |
| } |
| /// Environment variable name for the directory containing NVMe emulator | ||
| /// scripts (Register-NvmeEmulator.ps1, Add-NvmeEmulatorDevice.ps1, | ||
| /// NvmeEmulatorCommon.psm1). | ||
| pub const PETRI_NVME_EMULATOR_SCRIPTS_DIR: &str = "PETRI_NVME_EMULATOR_SCRIPTS_DIR"; | ||
|
|
||
| /// Returns the path to the NVMe emulator scripts directory, read from the | ||
| /// `PETRI_NVME_EMULATOR_SCRIPTS_DIR` environment variable. Returns an error if the | ||
| /// env var is unset or the required scripts are missing. |
There was a problem hiding this comment.
New environment variable PETRI_NVME_EMULATOR_SCRIPTS_DIR is required to run the NVMe emulator-backed tests. Please add/update developer documentation (likely Guide/src/dev_guide/tests/vmm.md or related) to describe how to set it up and what scripts are expected in that directory.
| let vhd_args: Vec<String> = vhd_paths.iter().map(|p| p.display().to_string()).collect(); | ||
| let vhd_refs: Vec<&str> = vhd_args.iter().map(|s| s.as_str()).collect(); | ||
| let script_str = format!("& \"{}\"", script.display()); | ||
| let output = run_host_cmd( | ||
| PowerShellBuilder::new() | ||
| .cmdlet(script_str) | ||
| .arg("VmName", vm_name) | ||
| .arg("VhdPaths", ps::Array::new(&vhd_refs)) |
There was a problem hiding this comment.
run_add_nvme_emulator_device converts PathBufs to String via display() and then builds &str references, which adds allocations and can be lossy. Since powershell_builder supports Path/PathBuf directly, consider passing vhd_paths directly to ps::Array::new(...) to avoid conversion.
| let vhd_args: Vec<String> = vhd_paths.iter().map(|p| p.display().to_string()).collect(); | |
| let vhd_refs: Vec<&str> = vhd_args.iter().map(|s| s.as_str()).collect(); | |
| let script_str = format!("& \"{}\"", script.display()); | |
| let output = run_host_cmd( | |
| PowerShellBuilder::new() | |
| .cmdlet(script_str) | |
| .arg("VmName", vm_name) | |
| .arg("VhdPaths", ps::Array::new(&vhd_refs)) | |
| let script_str = format!("& \"{}\"", script.display()); | |
| let output = run_host_cmd( | |
| PowerShellBuilder::new() | |
| .cmdlet(script_str) | |
| .arg("VmName", vm_name) | |
| .arg("VhdPaths", ps::Array::new(vhd_paths)) |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| crate::VmbusStorageType::Nvme => { | ||
| let mut vhd_paths = Vec::new(); | ||
| for Drive { disk, is_dvd: _ } in controller.drives.values() { | ||
| let vhd = petri_disk_to_hyperv(disk.as_ref(), &temp_dir).await?; | ||
| if let Some(path) = vhd { | ||
| vhd_paths.push(path); | ||
| } | ||
| } |
There was a problem hiding this comment.
For NVMe controllers, controller.drives is a HashMap<u32, Drive>, but this code builds vhd_paths by iterating controller.drives.values(). That drops the namespace IDs and also makes the VHD ordering nondeterministic, which can cause the emulator to assign namespaces to the wrong VHD (breaking nvme_namespace_id/LUN mapping and making tests flaky). Build the VHD list in a deterministic order keyed by the namespace ID (and validate the IDs match what the PowerShell script expects), rather than iterating values().
| pub fn add_nvme_emulator_lun_mapping( | ||
| mut self, | ||
| nvme_vsid: Guid, | ||
| nvme_namespace_id: u32, | ||
| scsi_controller_instance_id: Guid, | ||
| scsi_lun: u32, | ||
| ) -> Self { | ||
| self.config | ||
| .nvme_emulator_lun_mappings | ||
| .push(NvmeEmulatorLunMapping { | ||
| nvme_vsid, | ||
| nvme_namespace_id, | ||
| scsi_controller_instance_id, | ||
| scsi_lun, | ||
| }); | ||
| self |
There was a problem hiding this comment.
add_nvme_emulator_lun_mapping only records the mapping, but it doesn’t ensure that OpenHCL VTL2 settings are initialized in the firmware config. The Hyper-V backend’s pre-start fixup path currently relies on firmware.openhcl_config().vtl2_settings being Some(...) to push corrected settings into the VM, so a caller that adds a mapping without otherwise touching VTL2 settings can end up with the mapping never being applied to the running VM. Consider initializing vtl2_settings here (e.g., via self.config.firmware.vtl2_settings() / default settings) and/or rejecting this API for non-OpenHCL firmware.
| /// Environment variable name for the directory containing NVMe emulator | ||
| /// scripts (Register-NvmeEmulator.ps1, Add-NvmeEmulatorDevice.ps1, | ||
| /// NvmeEmulatorCommon.psm1). | ||
| pub const PETRI_NVME_EMULATOR_SCRIPTS_DIR: &str = "PETRI_NVME_EMULATOR_SCRIPTS_DIR"; | ||
|
|
||
| /// Returns the path to the NVMe emulator scripts directory, read from the | ||
| /// `PETRI_NVME_EMULATOR_SCRIPTS_DIR` environment variable. Returns an error if the | ||
| /// env var is unset or the required scripts are missing. | ||
| pub fn get_nvme_emulator_scripts_dir() -> anyhow::Result<PathBuf> { | ||
| let dir = std::env::var(PETRI_NVME_EMULATOR_SCRIPTS_DIR).map_err(|_| { | ||
| anyhow::anyhow!( | ||
| "NVMe emulator scripts not found. Set {PETRI_NVME_EMULATOR_SCRIPTS_DIR} to the \ | ||
| directory containing NVMe emulation management scripts \ | ||
| (Register-NvmeEmulator.ps1, Add-NvmeEmulatorDevice.ps1, NvmeEmulatorCommon.psm1)." |
There was a problem hiding this comment.
This introduces a new Hyper-V/Petri configuration env var (PETRI_NVME_EMULATOR_SCRIPTS_DIR) that’s required to run the NVMe emulator path. The repo Guide already documents several PETRI_* variables for running VMM tests; please add this variable (and the required script names) to the relevant Guide page so others can run the new test locally/under CI images without reading code.
Adds coverage for HyperV NVMe devices leveraging a closed source emulator
This test is currently marked as unstable for two reasons: