Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion petri/src/vm/hyperv/hyperv.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,21 @@ function New-CustomVM
# }
[hashtable] $ScsiControllers = $null,

# must be a hashtable with format:
# NvmeControllers => {
# Vsid => {
# Vtl,
# Drives => [
# DiskPath,
# ...
# ]
# },
Comment on lines +226 to +233
# ...
# }
# Drives are pre-sorted by NSID. The emulator assigns NSIDs 1..N
# by argument order.
[hashtable] $NvmeControllers = $null,

# must be a hashtable with format:
# IdeControllers => {
# ControllerNumber => {
Expand Down Expand Up @@ -352,6 +367,26 @@ function New-CustomVM
}
}

if ($NvmeControllers) {
if (-not (Get-Module -ListAvailable HvlDeviceHost)) {
throw ("NVMe emulator support requires the HvlDeviceHost " +
"PowerShell module. Ensure hvldevicehost.dll is installed " +
"and the module is available on this host.")
}
Import-Module HvlDeviceHost -ErrorAction Stop
Register-HvlDeviceHostClsid $CLSID_FIOV_NVME
foreach ($controller in $NvmeControllers.GetEnumerator()) {
$vsid = $controller.Name
$targetVtl = $controller.Value["Vtl"]
$vhdPaths = $controller.Value["Drives"]
$resourceSettings += New-NvmeEmulatorRasd `
-VhdPaths $vhdPaths `
-TargetVtl $targetVtl `
-Vsid ([Guid]$vsid) `
| ConvertTo-CimEmbeddedString
}
}

$vm = ($vmms | Invoke-CimMethod -Name "DefineSystem" -Arguments @{
"SystemSettings" = ($vssd | ConvertTo-CimEmbeddedString);
"ResourceSettings" = $resourceSettings
Expand Down Expand Up @@ -1418,4 +1453,4 @@ function Get-CimInstancePath {
)

return $path
}
}
59 changes: 45 additions & 14 deletions petri/src/vm/hyperv/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,8 @@ impl PetriVmmBackend for HyperVPetriBackend {
}
}

// Map SCSI
let mut scsi_controllers = HashMap::new();
// Map VMBus storage controllers (SCSI and NVMe).
let mut storage_controllers = HashMap::new();
for (
vsid,
VmbusStorageController {
Expand All @@ -232,10 +232,6 @@ impl PetriVmmBackend for HyperVPetriBackend {
},
) in config.vmbus_storage_controllers.iter()
{
if !matches!(controller_type, crate::VmbusStorageType::Scsi) {
todo!("other storage types for hyper-v")
}

let mut hyperv_drives = HashMap::new();
for (lun, Drive { disk, is_dvd }) in drives {
Comment thread
tjones60 marked this conversation as resolved.
hyperv_drives.insert(
Expand All @@ -246,9 +242,32 @@ impl PetriVmmBackend for HyperVPetriBackend {
},
);
}
scsi_controllers.insert(

let vmbus_controller_type = match controller_type {
crate::VmbusStorageType::Scsi => powershell::HyperVVmbusStorageType::Scsi,
crate::VmbusStorageType::Nvme => {
for (nsid, drive) in &hyperv_drives {
if drive.is_dvd {
anyhow::bail!("NVMe emulator does not support DVD drives");
}
if drive.disk.is_none() {
anyhow::bail!("NVMe drive cannot be empty (NSID {})", nsid);
}
}
powershell::HyperVVmbusStorageType::Nvme
}
_ => {
todo!(
"storage type {:?} not yet supported for hyper-v",
controller_type
)
}
Comment on lines +259 to +264
};

storage_controllers.insert(
*vsid,
powershell::HyperVScsiController {
powershell::HyperVVmbusStorageController {
controller_type: vmbus_controller_type,
target_vtl: *target_vtl,
drives: hyperv_drives,
},
Expand Down Expand Up @@ -277,6 +296,13 @@ impl PetriVmmBackend for HyperVPetriBackend {
}
}

let nvme_disk_paths: Vec<PathBuf> = storage_controllers
.values()
.filter(|c| matches!(c.controller_type, powershell::HyperVVmbusStorageType::Nvme))
.flat_map(|c| c.drives.values())
.filter_map(|drive| drive.disk.clone())
.collect();

// Attempt to enable COM3 and use that to get KMSG logs, otherwise
// fall back to use diag_client.
let supports_com3 = {
Expand Down Expand Up @@ -338,8 +364,7 @@ impl PetriVmmBackend for HyperVPetriBackend {
firmware_file: igvm_file.clone(),
firmware_parameters: openhcl_command_line,
guest_state_path,
scsi_controllers,
ide_controllers,
storage_controllers,
com_3: supports_com3,
imc_hiv,
management_vtl_settings,
Comment thread
babayet2 marked this conversation as resolved.
Expand All @@ -354,7 +379,7 @@ impl PetriVmmBackend for HyperVPetriBackend {
let local_path = igvm_file.as_ref().unwrap();
fs_err::copy(config.firmware.openhcl_firmware().unwrap(), local_path)
.context("failed to copy igvm file")?;
acl_read_for_vm(local_path, Some(*vm.vmid()))
acl_for_vm(local_path, Some(*vm.vmid()), false)
.context("failed to set ACL for igvm file")?;

let openhcl_log_file = log_source.log_file("openhcl")?;
Expand Down Expand Up @@ -383,6 +408,11 @@ impl PetriVmmBackend for HyperVPetriBackend {
}
}

// Grant the VM access to NVMe VHDs
for path in &nvme_disk_paths {
acl_for_vm(path, Some(*vm.vmid()), true).context("failed to set ACL for nvme VHD")?;
}

let serial_pipe_path = vm.get_vm_com_port_path(1);
let serial_log_file = log_source.log_file("guest")?;
log_tasks.push(driver.spawn(
Expand Down Expand Up @@ -570,14 +600,15 @@ impl PetriVmRuntime for HyperVPetriRuntime {
}
}

fn acl_read_for_vm(path: &Path, id: Option<Guid>) -> anyhow::Result<()> {
fn acl_for_vm(path: &Path, id: Option<Guid>, write: bool) -> anyhow::Result<()> {
let sid_arg = format!(
"NT VIRTUAL MACHINE\\{name}:R",
"NT VIRTUAL MACHINE\\{name}:{perm}",
name = if let Some(id) = id {
format!("{id:X}")
} else {
"Virtual Machines".to_string()
}
},
perm = if write { 'M' } else { 'R' }
);
Comment on lines +603 to 612
let output = std::process::Command::new("icacls.exe")
.arg(path)
Expand Down
108 changes: 92 additions & 16 deletions petri/src/vm/hyperv/powershell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use crate::OpenHclServicingFlags;
use crate::PetriVmConfig;
use crate::PetriVmProperties;
use crate::VmScreenshotMeta;
use crate::Vtl;
use crate::run_host_cmd;
use crate::vm::append_cmdline;
use anyhow::Context;
Expand Down Expand Up @@ -290,8 +289,8 @@ pub struct HyperVNewCustomVMArgs {
pub hw_threads_per_core: Option<u64>,
/// Processors per socket
pub max_processors_per_numa_node: Option<u64>,
/// SCSI controllers and associated drives/disks
pub scsi_controllers: HashMap<Guid, HyperVScsiController>,
/// VMBus storage controllers (SCSI and NVMe), keyed by VSID
pub storage_controllers: HashMap<Guid, HyperVVmbusStorageController>,
/// IDE controllers and associated drives/disks
pub ide_controllers: HashMap<u32, HashMap<u8, HyperVDrive>>,
/// Temporary file containing initial machine configuration data
Expand All @@ -306,11 +305,21 @@ pub struct HyperVNewCustomVMArgs {
pub management_vtl_settings: Option<NamedTempFile>,
}

/// Hyper-V SCSI controller
pub struct HyperVScsiController {
/// The VTL to assign the storage controller to
pub target_vtl: Vtl,
/// Drives (with any inserted disks) attached to this storage controller
/// VMBus storage controller type
pub enum HyperVVmbusStorageType {
/// SCSI controller (Msvm_ResourceAllocationSettingData)
Scsi,
/// NVMe emulator controller (created via closed-source HvlDeviceHost module)
Nvme,
}

/// VMBus storage controller configuration (SCSI or NVMe), keyed by VSID.
pub struct HyperVVmbusStorageController {
/// Controller type
pub controller_type: HyperVVmbusStorageType,
/// Target VTL
pub target_vtl: crate::Vtl,
/// Drives attached to this controller, keyed by LUN (SCSI) or namespace ID (NVMe).
pub drives: HashMap<u32, HyperVDrive>,
}

Expand Down Expand Up @@ -565,7 +574,7 @@ impl HyperVNewCustomVMArgs {
firmware_file: None,
firmware_parameters: None,
guest_state_path: None,
scsi_controllers: HashMap::new(),
storage_controllers: HashMap::new(),
ide_controllers: HashMap::new(),
com_3: false,
imc_hiv: None,
Expand Down Expand Up @@ -596,9 +605,28 @@ pub async fn run_new_customvm(ps_mod: &Path, args: HyperVNewCustomVMArgs) -> any
}
});

let scsi_controllers = (!args.scsi_controllers.is_empty()).then(|| {
ps::HashTable::new(args.scsi_controllers.into_iter().map(
|(vsid, HyperVScsiController { target_vtl, drives })| {
// Partition storage controllers into SCSI and NVMe.
let mut scsi_map: HashMap<Guid, HyperVVmbusStorageController> = HashMap::new();
let mut nvme_map: HashMap<Guid, HyperVVmbusStorageController> = HashMap::new();
for (vsid, controller) in args.storage_controllers {
match controller.controller_type {
Comment thread
babayet2 marked this conversation as resolved.
HyperVVmbusStorageType::Scsi => {
scsi_map.insert(vsid, controller);
}
HyperVVmbusStorageType::Nvme => {
Comment thread
babayet2 marked this conversation as resolved.
nvme_map.insert(vsid, controller);
}
}
Comment thread
babayet2 marked this conversation as resolved.
}

let scsi_controllers = (!scsi_map.is_empty()).then(|| {
ps::HashTable::new(scsi_map.into_iter().map(
|(
vsid,
HyperVVmbusStorageController {
target_vtl, drives, ..
},
)| {
(
format!("\"{vsid}\""),
ps::Value::new(ps::HashTable::new([
Expand Down Expand Up @@ -645,11 +673,58 @@ pub async fn run_new_customvm(ps_mod: &Path, args: HyperVNewCustomVMArgs) -> any
))
});

// Serialize NVMe controllers as a hashtable keyed by VSID.
// Each value: @{ Vtl = N; Drives = @("path1", "path2", ...) }
// New-CustomVM imports HvlDeviceHost internally and calls New-NvmeEmulatorRasd.
let nvme_controllers = if nvme_map.is_empty() {
None
} else {
let mut nvme_entries = Vec::new();
for (
vsid,
HyperVVmbusStorageController {
target_vtl, drives, ..
},
) in nvme_map
{
// Sort drives by namespace ID and validate they are exactly
// 1..N — the emulator assigns NSIDs sequentially by VHD
Comment thread
babayet2 marked this conversation as resolved.
// argument order.
let mut sorted_drives: Vec<_> = drives.into_iter().collect();
sorted_drives.sort_by_key(|(nsid, _)| *nsid);
let expected: Vec<u32> = (1..=sorted_drives.len() as u32).collect();
let actual: Vec<u32> = sorted_drives.iter().map(|(nsid, _)| *nsid).collect();
anyhow::ensure!(
actual == expected,
"NVMe namespace IDs must be 1..{}, got {:?}",
Comment thread
tjones60 marked this conversation as resolved.
expected.len(),
actual
);
nvme_entries.push((
Comment on lines +690 to +703
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NVMe controllers with an empty drives map will currently serialize as Drives = @() and pass the actual == expected check (both empty). That likely results in a confusing failure from New-NvmeEmulatorRasd. Consider explicitly rejecting empty NVMe controllers with a clear error (including VSID).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the behavior if you have an empty NVMe controller? Is that valid?

format!("\"{vsid}\""),
ps::Value::new(ps::HashTable::new([
("Vtl", ps::Value::new(target_vtl as u32)),
(
"Drives",
ps::Value::new(ps::Array::new(sorted_drives.into_iter().map(
|(_, HyperVDrive { disk, .. })| {
disk.expect("NVMe drives must have disk paths")
},
))),
),
Comment thread
tjones60 marked this conversation as resolved.
])),
));
}
Some(ps::HashTable::new(nvme_entries))
};

let builder = PowerShellBuilder::new()
.cmdlet("Import-Module")
.positional(ps_mod)
.next();

let vmid = run_host_cmd(
PowerShellBuilder::new()
.cmdlet("Import-Module")
.positional(ps_mod)
.next()
builder
.cmdlet("New-CustomVM")
.arg("VMName", args.name)
.arg_opt("Generation", args.generation)
Expand Down Expand Up @@ -686,6 +761,7 @@ pub async fn run_new_customvm(ps_mod: &Path, args: HyperVNewCustomVMArgs) -> any
)
.arg_opt("ScsiControllers", scsi_controllers)
.arg_opt("IdeControllers", ide_controllers)
.arg_opt("NvmeControllers", nvme_controllers)
.arg_opt("ImcHive", args.imc_hiv.as_ref().map(|f| f.path()))
.arg("Com1", args.com_1)
.arg("Com3", args.com_3)
Expand Down
Loading
Loading