@@ -8,7 +8,6 @@ use crate::OpenHclServicingFlags;
88use crate :: PetriVmConfig ;
99use crate :: PetriVmProperties ;
1010use crate :: VmScreenshotMeta ;
11- use crate :: Vtl ;
1211use crate :: run_host_cmd;
1312use crate :: vm:: append_cmdline;
1413use 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 )
0 commit comments