@@ -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,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 )
0 commit comments