Skip to content

Commit 87af4b4

Browse files
committed
petri: add NVMe emulator support for Hyper-V
1 parent 89f2bc2 commit 87af4b4

4 files changed

Lines changed: 242 additions & 28 deletions

File tree

petri/src/vm/hyperv/hyperv.psm1

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,19 @@ 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+
[hashtable] $NvmeControllers = $null,
237+
225238
# must be a hashtable with format:
226239
# IdeControllers => {
227240
# ControllerNumber => {
@@ -352,6 +365,26 @@ function New-CustomVM
352365
}
353366
}
354367

368+
if ($NvmeControllers) {
369+
Import-Module HvlDeviceHost
370+
foreach ($controller in $NvmeControllers.GetEnumerator()) {
371+
$vsid = $controller.Name
372+
$targetVtl = $controller.Value["Vtl"]
373+
$drives = $controller.Value["Drives"]
374+
# Sort drives by NSID to produce ordered VHD paths — the emulator
375+
# assigns NSIDs 1..N by argument order.
376+
#
377+
# TODO explicit NSID mapping when emulator supports --nsid
378+
$sortedDrives = $drives.GetEnumerator() | Sort-Object { [int]$_.Name }
379+
$vhdPaths = @($sortedDrives | ForEach-Object { $_.Value })
380+
$resourceSettings += New-NvmeEmulatorRasd `
381+
-VhdPaths $vhdPaths `
382+
-TargetVtl $targetVtl `
383+
-Vsid ([Guid]$vsid) `
384+
| ConvertTo-CimEmbeddedString
385+
}
386+
}
387+
355388
$vm = ($vmms | Invoke-CimMethod -Name "DefineSystem" -Arguments @{
356389
"SystemSettings" = ($vssd | ConvertTo-CimEmbeddedString);
357390
"ResourceSettings" = $resourceSettings
@@ -1418,4 +1451,4 @@ function Get-CimInstancePath {
14181451
)
14191452

14201453
return $path
1421-
}
1454+
}

petri/src/vm/hyperv/mod.rs

Lines changed: 53 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,9 +232,16 @@ 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-
}
235+
let vtl_num = match target_vtl {
236+
crate::Vtl::Vtl0 => 0u8,
237+
crate::Vtl::Vtl2 => 2u8,
238+
_ => {
239+
anyhow::bail!(
240+
"unsupported VTL {:?} for storage controller",
241+
target_vtl
242+
)
243+
}
244+
};
238245

239246
let mut hyperv_drives = HashMap::new();
240247
for (lun, Drive { disk, is_dvd }) in drives {
@@ -246,10 +253,47 @@ impl PetriVmmBackend for HyperVPetriBackend {
246253
},
247254
);
248255
}
249-
scsi_controllers.insert(
256+
257+
let vmbus_controller_type = match controller_type {
258+
crate::VmbusStorageType::Scsi => powershell::HyperVVmbusStorageType::Scsi,
259+
crate::VmbusStorageType::Nvme => {
260+
// Validate NVMe constraints on drives.
261+
for (nsid, drive) in &hyperv_drives {
262+
if drive.is_dvd {
263+
anyhow::bail!("NVMe emulator does not support DVD drives");
264+
}
265+
if drive.disk.is_none() {
266+
anyhow::bail!("NVMe drive cannot be empty (NSID {})", nsid);
267+
}
268+
}
269+
// The emulator assigns NSIDs sequentially (1..N) by VHD
270+
// argument order. Validate keys are exactly 1..N.
271+
//
272+
// TODO the emulator should accept NSID/VHD pairs
273+
let expected: Vec<u32> = (1..=hyperv_drives.len() as u32).collect();
274+
let mut actual: Vec<u32> = hyperv_drives.keys().copied().collect();
275+
actual.sort();
276+
anyhow::ensure!(
277+
actual == expected,
278+
"NVMe namespace IDs must be 1..{}, got {:?}",
279+
expected.len(),
280+
actual
281+
);
282+
powershell::HyperVVmbusStorageType::Nvme
283+
}
284+
_ => {
285+
todo!(
286+
"storage type {:?} not yet supported for hyper-v",
287+
controller_type
288+
)
289+
}
290+
};
291+
292+
storage_controllers.insert(
250293
*vsid,
251-
powershell::HyperVScsiController {
252-
target_vtl: *target_vtl,
294+
powershell::HyperVVmbusStorageController {
295+
controller_type: vmbus_controller_type,
296+
target_vtl: vtl_num,
253297
drives: hyperv_drives,
254298
},
255299
);
@@ -338,8 +382,7 @@ impl PetriVmmBackend for HyperVPetriBackend {
338382
firmware_file: igvm_file.clone(),
339383
firmware_parameters: openhcl_command_line,
340384
guest_state_path,
341-
scsi_controllers,
342-
ide_controllers,
385+
storage_controllers,
343386
com_3: supports_com3,
344387
imc_hiv,
345388
management_vtl_settings,

petri/src/vm/hyperv/powershell.rs

Lines changed: 78 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,22 @@ 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 (0 or 2)
321+
pub target_vtl: u8,
322+
/// Drives attached to this controller, keyed by LUN (SCSI) or namespace ID (NVMe).
323+
/// For NVMe, keys must be exactly 1..N.
314324
pub drives: HashMap<u32, HyperVDrive>,
315325
}
316326

@@ -565,7 +575,7 @@ impl HyperVNewCustomVMArgs {
565575
firmware_file: None,
566576
firmware_parameters: None,
567577
guest_state_path: None,
568-
scsi_controllers: HashMap::new(),
578+
storage_controllers: HashMap::new(),
569579
ide_controllers: HashMap::new(),
570580
com_3: false,
571581
imc_hiv: None,
@@ -596,9 +606,23 @@ pub async fn run_new_customvm(ps_mod: &Path, args: HyperVNewCustomVMArgs) -> any
596606
}
597607
});
598608

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 })| {
609+
// Partition storage controllers into SCSI and NVMe.
610+
let mut scsi_map: HashMap<Guid, HyperVVmbusStorageController> = HashMap::new();
611+
let mut nvme_map: HashMap<Guid, HyperVVmbusStorageController> = HashMap::new();
612+
for (vsid, controller) in args.storage_controllers {
613+
match controller.controller_type {
614+
HyperVVmbusStorageType::Scsi => {
615+
scsi_map.insert(vsid, controller);
616+
}
617+
HyperVVmbusStorageType::Nvme => {
618+
nvme_map.insert(vsid, controller);
619+
}
620+
}
621+
}
622+
623+
let scsi_controllers = (!scsi_map.is_empty()).then(|| {
624+
ps::HashTable::new(scsi_map.into_iter().map(
625+
|(vsid, HyperVVmbusStorageController { target_vtl, drives, .. })| {
602626
(
603627
format!("\"{vsid}\""),
604628
ps::Value::new(ps::HashTable::new([
@@ -645,11 +669,48 @@ pub async fn run_new_customvm(ps_mod: &Path, args: HyperVNewCustomVMArgs) -> any
645669
))
646670
});
647671

672+
// Serialize NVMe controllers as a hashtable keyed by VSID.
673+
// Each value: @{ Vtl = N; Drives = @{ Nsid = "DiskPath"; ... } }
674+
// New-CustomVM imports HvlDeviceHost internally and calls New-NvmeEmulatorRasd.
675+
let nvme_controllers = (!nvme_map.is_empty()).then(|| {
676+
ps::HashTable::new(nvme_map.into_iter().map(
677+
|(vsid, HyperVVmbusStorageController { target_vtl, drives, .. })| {
678+
// Sort drives by key (namespace ID) to produce ordered VHD
679+
// paths — the emulator assigns NSIDs 1..N by argument order.
680+
let mut sorted_drives: Vec<_> = drives.into_iter().collect();
681+
sorted_drives.sort_by_key(|(nsid, _)| *nsid);
682+
(
683+
// VSID without braces — VMMS InitializeValues calls
684+
// UuidFromString which rejects the {}-wrapped format.
685+
format!("\"{vsid}\""),
686+
ps::Value::new(ps::HashTable::new([
687+
("Vtl", ps::Value::new(target_vtl as u32)),
688+
(
689+
"Drives",
690+
ps::Value::new(ps::HashTable::new(sorted_drives.into_iter().map(
691+
|(nsid, HyperVDrive { disk, .. })| {
692+
(
693+
nsid.to_string(),
694+
ps::Value::new(
695+
disk.expect("NVMe drives must have disk paths"),
696+
),
697+
)
698+
},
699+
))),
700+
),
701+
])),
702+
)
703+
},
704+
))
705+
});
706+
707+
let builder = PowerShellBuilder::new()
708+
.cmdlet("Import-Module")
709+
.positional(ps_mod)
710+
.next();
711+
648712
let vmid = run_host_cmd(
649-
PowerShellBuilder::new()
650-
.cmdlet("Import-Module")
651-
.positional(ps_mod)
652-
.next()
713+
builder
653714
.cmdlet("New-CustomVM")
654715
.arg("VMName", args.name)
655716
.arg_opt("Generation", args.generation)
@@ -686,6 +747,7 @@ pub async fn run_new_customvm(ps_mod: &Path, args: HyperVNewCustomVMArgs) -> any
686747
)
687748
.arg_opt("ScsiControllers", scsi_controllers)
688749
.arg_opt("IdeControllers", ide_controllers)
750+
.arg_opt("NvmeControllers", nvme_controllers)
689751
.arg_opt("ImcHive", args.imc_hiv.as_ref().map(|f| f.path()))
690752
.arg("Com1", args.com_1)
691753
.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)