Skip to content

Commit a176438

Browse files
committed
petri: add NVMe emulator support for Hyper-V
1 parent 2532394 commit a176438

4 files changed

Lines changed: 224 additions & 28 deletions

File tree

petri/src/vm/hyperv/hyperv.psm1

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,21 @@ function New-CustomVM
222222
# }
223223
[hashtable] $ScsiControllers = $null,
224224

225+
# must be a hashtable with format:
226+
# NvmeControllers => {
227+
# Vsid => {
228+
# Vtl,
229+
# Drives => @(
230+
# @{ Nsid; DiskPath },
231+
# ...
232+
# )
233+
# },
234+
# ...
235+
# }
236+
# Drives are pre-sorted by NSID. The emulator assigns NSIDs 1..N
237+
# by argument order.
238+
[hashtable] $NvmeControllers = $null,
239+
225240
# must be a hashtable with format:
226241
# IdeControllers => {
227242
# ControllerNumber => {
@@ -352,6 +367,22 @@ function New-CustomVM
352367
}
353368
}
354369

370+
if ($NvmeControllers) {
371+
Import-Module HvlDeviceHost
372+
foreach ($controller in $NvmeControllers.GetEnumerator()) {
373+
$vsid = $controller.Name
374+
$targetVtl = $controller.Value["Vtl"]
375+
$drives = $controller.Value["Drives"]
376+
# Drives arrive pre-sorted by NSID from the Rust layer.
377+
$vhdPaths = @($drives | ForEach-Object { $_["DiskPath"] })
378+
$resourceSettings += New-NvmeEmulatorRasd `
379+
-VhdPaths $vhdPaths `
380+
-TargetVtl $targetVtl `
381+
-Vsid ([Guid]$vsid) `
382+
| ConvertTo-CimEmbeddedString
383+
}
384+
}
385+
355386
$vm = ($vmms | Invoke-CimMethod -Name "DefineSystem" -Arguments @{
356387
"SystemSettings" = ($vssd | ConvertTo-CimEmbeddedString);
357388
"ResourceSettings" = $resourceSettings
@@ -1418,4 +1449,4 @@ function Get-CimInstancePath {
14181449
)
14191450

14201451
return $path
1421-
}
1452+
}

petri/src/vm/hyperv/mod.rs

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -221,8 +221,8 @@ impl PetriVmmBackend for HyperVPetriBackend {
221221
}
222222
}
223223

224-
// Map SCSI
225-
let mut scsi_controllers = HashMap::new();
224+
// Map VMBus storage controllers (SCSI and NVMe).
225+
let mut storage_controllers = HashMap::new();
226226
for (
227227
vsid,
228228
VmbusStorageController {
@@ -232,10 +232,6 @@ impl PetriVmmBackend for HyperVPetriBackend {
232232
},
233233
) in config.vmbus_storage_controllers.iter()
234234
{
235-
if !matches!(controller_type, crate::VmbusStorageType::Scsi) {
236-
todo!("other storage types for hyper-v")
237-
}
238-
239235
let mut hyperv_drives = HashMap::new();
240236
for (lun, Drive { disk, is_dvd }) in drives {
241237
hyperv_drives.insert(
@@ -246,9 +242,32 @@ impl PetriVmmBackend for HyperVPetriBackend {
246242
},
247243
);
248244
}
249-
scsi_controllers.insert(
245+
246+
let vmbus_controller_type = match controller_type {
247+
crate::VmbusStorageType::Scsi => powershell::HyperVVmbusStorageType::Scsi,
248+
crate::VmbusStorageType::Nvme => {
249+
for (nsid, drive) in &hyperv_drives {
250+
if drive.is_dvd {
251+
anyhow::bail!("NVMe emulator does not support DVD drives");
252+
}
253+
if drive.disk.is_none() {
254+
anyhow::bail!("NVMe drive cannot be empty (NSID {})", nsid);
255+
}
256+
}
257+
powershell::HyperVVmbusStorageType::Nvme
258+
}
259+
_ => {
260+
todo!(
261+
"storage type {:?} not yet supported for hyper-v",
262+
controller_type
263+
)
264+
}
265+
};
266+
267+
storage_controllers.insert(
250268
*vsid,
251-
powershell::HyperVScsiController {
269+
powershell::HyperVVmbusStorageController {
270+
controller_type: vmbus_controller_type,
252271
target_vtl: *target_vtl,
253272
drives: hyperv_drives,
254273
},
@@ -338,8 +357,7 @@ impl PetriVmmBackend for HyperVPetriBackend {
338357
firmware_file: igvm_file.clone(),
339358
firmware_parameters: openhcl_command_line,
340359
guest_state_path,
341-
scsi_controllers,
342-
ide_controllers,
360+
storage_controllers,
343361
com_3: supports_com3,
344362
imc_hiv,
345363
management_vtl_settings,

petri/src/vm/hyperv/powershell.rs

Lines changed: 87 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ use crate::OpenHclServicingFlags;
88
use crate::PetriVmConfig;
99
use crate::PetriVmProperties;
1010
use crate::VmScreenshotMeta;
11-
use crate::Vtl;
1211
use crate::run_host_cmd;
1312
use crate::vm::append_cmdline;
1413
use anyhow::Context;
@@ -290,8 +289,8 @@ pub struct HyperVNewCustomVMArgs {
290289
pub hw_threads_per_core: Option<u64>,
291290
/// Processors per socket
292291
pub max_processors_per_numa_node: Option<u64>,
293-
/// SCSI controllers and associated drives/disks
294-
pub scsi_controllers: HashMap<Guid, HyperVScsiController>,
292+
/// VMBus storage controllers (SCSI and NVMe), keyed by VSID
293+
pub storage_controllers: HashMap<Guid, HyperVVmbusStorageController>,
295294
/// IDE controllers and associated drives/disks
296295
pub ide_controllers: HashMap<u32, HashMap<u8, HyperVDrive>>,
297296
/// Temporary file containing initial machine configuration data
@@ -306,11 +305,21 @@ pub struct HyperVNewCustomVMArgs {
306305
pub management_vtl_settings: Option<NamedTempFile>,
307306
}
308307

309-
/// Hyper-V SCSI controller
310-
pub struct HyperVScsiController {
311-
/// The VTL to assign the storage controller to
312-
pub target_vtl: Vtl,
313-
/// Drives (with any inserted disks) attached to this storage controller
308+
/// VMBus storage controller type
309+
pub enum HyperVVmbusStorageType {
310+
/// SCSI controller (Msvm_ResourceAllocationSettingData)
311+
Scsi,
312+
/// NVMe emulator controller (created via closed-source HvlDeviceHost module)
313+
Nvme,
314+
}
315+
316+
/// VMBus storage controller configuration (SCSI or NVMe), keyed by VSID.
317+
pub struct HyperVVmbusStorageController {
318+
/// Controller type
319+
pub controller_type: HyperVVmbusStorageType,
320+
/// Target VTL
321+
pub target_vtl: crate::Vtl,
322+
/// Drives attached to this controller, keyed by LUN (SCSI) or namespace ID (NVMe).
314323
pub drives: HashMap<u32, HyperVDrive>,
315324
}
316325

@@ -565,7 +574,7 @@ impl HyperVNewCustomVMArgs {
565574
firmware_file: None,
566575
firmware_parameters: None,
567576
guest_state_path: None,
568-
scsi_controllers: HashMap::new(),
577+
storage_controllers: HashMap::new(),
569578
ide_controllers: HashMap::new(),
570579
com_3: false,
571580
imc_hiv: None,
@@ -596,9 +605,23 @@ pub async fn run_new_customvm(ps_mod: &Path, args: HyperVNewCustomVMArgs) -> any
596605
}
597606
});
598607

599-
let scsi_controllers = (!args.scsi_controllers.is_empty()).then(|| {
600-
ps::HashTable::new(args.scsi_controllers.into_iter().map(
601-
|(vsid, HyperVScsiController { target_vtl, drives })| {
608+
// Partition storage controllers into SCSI and NVMe.
609+
let mut scsi_map: HashMap<Guid, HyperVVmbusStorageController> = HashMap::new();
610+
let mut nvme_map: HashMap<Guid, HyperVVmbusStorageController> = HashMap::new();
611+
for (vsid, controller) in args.storage_controllers {
612+
match controller.controller_type {
613+
HyperVVmbusStorageType::Scsi => {
614+
scsi_map.insert(vsid, controller);
615+
}
616+
HyperVVmbusStorageType::Nvme => {
617+
nvme_map.insert(vsid, controller);
618+
}
619+
}
620+
}
621+
622+
let scsi_controllers = (!scsi_map.is_empty()).then(|| {
623+
ps::HashTable::new(scsi_map.into_iter().map(
624+
|(vsid, HyperVVmbusStorageController { target_vtl, drives, .. })| {
602625
(
603626
format!("\"{vsid}\""),
604627
ps::Value::new(ps::HashTable::new([
@@ -645,11 +668,58 @@ pub async fn run_new_customvm(ps_mod: &Path, args: HyperVNewCustomVMArgs) -> any
645668
))
646669
});
647670

671+
// Serialize NVMe controllers as a hashtable keyed by VSID.
672+
// Each value: @{ Vtl = N; Drives = @(@{Nsid = 1; DiskPath = "..."}, ...) }
673+
// New-CustomVM imports HvlDeviceHost internally and calls New-NvmeEmulatorRasd.
674+
let nvme_controllers = (!nvme_map.is_empty()).then(|| {
675+
ps::HashTable::new(nvme_map.into_iter().map(
676+
|(vsid, HyperVVmbusStorageController { target_vtl, drives, .. })| {
677+
// Sort drives by namespace ID and validate they are exactly
678+
// 1..N — the emulator assigns NSIDs sequentially by VHD
679+
// argument order.
680+
let mut sorted_drives: Vec<_> = drives.into_iter().collect();
681+
sorted_drives.sort_by_key(|(nsid, _)| *nsid);
682+
let expected: Vec<u32> = (1..=sorted_drives.len() as u32).collect();
683+
let actual: Vec<u32> = sorted_drives.iter().map(|(nsid, _)| *nsid).collect();
684+
assert_eq!(
685+
actual, expected,
686+
"NVMe namespace IDs must be 1..{}, got {:?}",
687+
expected.len(),
688+
actual
689+
);
690+
(
691+
format!("\"{vsid}\""),
692+
ps::Value::new(ps::HashTable::new([
693+
("Vtl", ps::Value::new(target_vtl as u32)),
694+
(
695+
"Drives",
696+
ps::Value::new(ps::Array::new(sorted_drives.into_iter().map(
697+
|(nsid, HyperVDrive { disk, .. })| {
698+
ps::HashTable::new([
699+
("Nsid", ps::Value::new(nsid)),
700+
(
701+
"DiskPath",
702+
ps::Value::new(
703+
disk.expect("NVMe drives must have disk paths"),
704+
),
705+
),
706+
])
707+
},
708+
))),
709+
),
710+
])),
711+
)
712+
},
713+
))
714+
});
715+
716+
let builder = PowerShellBuilder::new()
717+
.cmdlet("Import-Module")
718+
.positional(ps_mod)
719+
.next();
720+
648721
let vmid = run_host_cmd(
649-
PowerShellBuilder::new()
650-
.cmdlet("Import-Module")
651-
.positional(ps_mod)
652-
.next()
722+
builder
653723
.cmdlet("New-CustomVM")
654724
.arg("VMName", args.name)
655725
.arg_opt("Generation", args.generation)
@@ -686,6 +756,7 @@ pub async fn run_new_customvm(ps_mod: &Path, args: HyperVNewCustomVMArgs) -> any
686756
)
687757
.arg_opt("ScsiControllers", scsi_controllers)
688758
.arg_opt("IdeControllers", ide_controllers)
759+
.arg_opt("NvmeControllers", nvme_controllers)
689760
.arg_opt("ImcHive", args.imc_hiv.as_ref().map(|f| f.path()))
690761
.arg("Com1", args.com_1)
691762
.arg("Com3", args.com_3)

vmm_tests/vmm_tests/tests/tests/x86_64/storage.rs

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,83 @@ async fn storvsp_hyperv<T: PetriVmmBackend>(
381381
Ok(())
382382
}
383383

384-
/// Test an OpenHCL Linux Stripe VM with two SCSI disk assigned to VTL2 via NVMe Emulator
384+
/// Test a Hyper-V OpenHCL Linux VM with an NVMe emulator device assigned to
385+
/// VTL2, relayed to VTL0 via SCSI. Validates that the guest can discover and
386+
/// perform IO on the disk.
387+
#[cfg(windows)]
388+
#[vmm_test(unstable_hyperv_openhcl_uefi_x64(vhd(ubuntu_2504_server_x64)))]
389+
async fn storvsp_nvme_hyperv<T: PetriVmmBackend>(
390+
config: PetriVmBuilder<T>,
391+
) -> Result<(), anyhow::Error> {
392+
let vtl0_nvme_lun = 0;
393+
let nvme_nsid = 1;
394+
let nvme_vsid = Guid::new_random();
395+
let scsi_instance = Guid::new_random();
396+
const NVME_DISK_SECTORS: u64 = 0x5_0000;
397+
const SECTOR_SIZE: u64 = 512;
398+
const EXPECTED_NVME_DISK_SIZE_BYTES: u64 = NVME_DISK_SECTORS * SECTOR_SIZE;
399+
400+
// Assumptions made by test infra & routines:
401+
//
402+
// 1. Some test-infra added disks are 64MiB in size. Since we find disks by size,
403+
// ensure that our test disks are a different size.
404+
// 2. Disks under test need to be at least 100MiB for the IO tests (see [`test_storage_linux`]),
405+
// with some arbitrary buffer (5MiB in this case).
406+
static_assertions::const_assert_ne!(EXPECTED_NVME_DISK_SIZE_BYTES, 64 * 1024 * 1024);
407+
static_assertions::const_assert!(EXPECTED_NVME_DISK_SIZE_BYTES > 105 * 1024 * 1024);
408+
409+
let mut vhd =
410+
tempfile::NamedTempFile::with_suffix("nvme.vhd").context("create temp nvme vhd")?;
411+
vhd.as_file()
412+
.set_len(EXPECTED_NVME_DISK_SIZE_BYTES)
413+
.context("set file length")?;
414+
415+
disk_vhd1::Vhd1Disk::make_fixed(vhd.as_file_mut()).context("make fixed")?;
416+
417+
// Close the handle without deleting the file, so Hyper-V can open it.
418+
let vhd_path = vhd.into_temp_path();
419+
420+
let (vm, agent) = config
421+
.with_vmbus_redirect(true)
422+
.add_vmbus_storage_controller(&nvme_vsid, petri::Vtl::Vtl2, petri::VmbusStorageType::Nvme)
423+
.add_vmbus_drive(
424+
petri::Drive::new(Some(petri::Disk::Persistent(vhd_path.to_path_buf())), false),
425+
&nvme_vsid,
426+
Some(nvme_nsid),
427+
)
428+
.add_vtl2_storage_controller(
429+
Vtl2StorageControllerBuilder::new(ControllerType::Scsi)
430+
.with_instance_id(scsi_instance)
431+
.add_lun(
432+
Vtl2LunBuilder::disk()
433+
.with_location(vtl0_nvme_lun)
434+
.with_physical_device(Vtl2StorageBackingDeviceBuilder::new(
435+
ControllerType::Nvme,
436+
nvme_vsid,
437+
nvme_nsid,
438+
)),
439+
)
440+
.build(),
441+
)
442+
.run()
443+
.await?;
444+
445+
test_storage_linux(
446+
&agent,
447+
scsi_instance,
448+
vec![ExpectedGuestDevice {
449+
lun: vtl0_nvme_lun,
450+
disk_size_sectors: NVME_DISK_SECTORS as usize,
451+
friendly_name: "nvme".to_string(),
452+
}],
453+
)
454+
.await?;
455+
456+
agent.power_off().await?;
457+
vm.wait_for_clean_teardown().await?;
458+
459+
Ok(())
460+
}
385461
#[openvmm_test(
386462
openhcl_linux_direct_x64,
387463
openhcl_uefi_x64(vhd(ubuntu_2504_server_x64))

0 commit comments

Comments
 (0)