From bcbdb0ec80b510c050fda6226df22c931bdeab24 Mon Sep 17 00:00:00 2001 From: Hadi Orabi Date: Mon, 18 May 2026 04:37:57 +0000 Subject: [PATCH 1/5] plumbing for closed source nvme tests --- petri/src/vm/hyperv/Utilities.psm1 | 325 +++++++++++++++++ petri/src/vm/hyperv/hyperv.psm1 | 344 ++---------------- petri/src/vm/hyperv/mod.rs | 2 + petri/src/vm/hyperv/powershell.rs | 53 +++ petri/src/vm/mod.rs | 24 ++ petri/src/vm/openvmm/construct.rs | 5 + .../vmm_tests/tests/tests/x86_64/storage.rs | 1 + 7 files changed, 433 insertions(+), 321 deletions(-) create mode 100644 petri/src/vm/hyperv/Utilities.psm1 diff --git a/petri/src/vm/hyperv/Utilities.psm1 b/petri/src/vm/hyperv/Utilities.psm1 new file mode 100644 index 0000000000..50a2ad1239 --- /dev/null +++ b/petri/src/vm/hyperv/Utilities.psm1 @@ -0,0 +1,325 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Constants +# + +$ROOT_HYPER_V_NAMESPACE = "root\virtualization\v2" + +function Get-Vmms +{ + Get-CimInstance -Namespace $ROOT_HYPER_V_NAMESPACE -Class Msvm_VirtualSystemManagementService +} + +function Get-MsvmComputerSystem +{ + [CmdletBinding()] + Param ( + [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] + [System.Object] + $Vm + ) + + $vmid = $Vm.Id + $msvmComputerSystem = Get-CimInstance -namespace $ROOT_HYPER_V_NAMESPACE -query "select * from Msvm_ComputerSystem where Name = '$vmid'" + + if (-not $msvmComputerSystem) + { + throw "Unable to find a virtual machine with id $vmid." + } + + $msvmComputerSystem +} + +function Get-VmSystemSettings +{ + [CmdletBinding()] + Param ( + [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] + [System.Object] + $Vm + ) + + Get-MsvmComputerSystem $Vm | Get-CimAssociatedInstance -ResultClass "Msvm_VirtualSystemSettingData" -Association "Msvm_SettingsDefineState" +} + +function Add-VmResourceSettings { + param( + [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] + [System.Object] $Vm, + + [Parameter(Mandatory = $true)] + [Microsoft.Management.Infrastructure.CimInstance] $Rasd + ) + + $vssd = Get-VmSystemSettings $Vm + $vmms = Get-Vmms + $vmms | Invoke-CimMethod -Name "AddResourceSettings" -Arguments @{ + "AffectedConfiguration" = $vssd; + "ResourceSettings" = @($Rasd | ConvertTo-CimEmbeddedString) + } | Trace-CimMethodExecution -MethodName "AddResourceSettings" -CimInstance $vmms +} + +function ConvertTo-CimEmbeddedString +{ + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline)] + [Microsoft.Management.Infrastructure.CimInstance] $CimInstance + ) + + if ($null -eq $CimInstance) + { + return "" + } + + $cimSerializer = [Microsoft.Management.Infrastructure.Serialization.CimSerializer]::Create() + $serializedObj = $cimSerializer.Serialize($CimInstance, [Microsoft.Management.Infrastructure.Serialization.InstanceSerializationOptions]::None) + return [System.Text.Encoding]::Unicode.GetString($serializedObj) +} + +# CIM is strict and won't let you write read-only properties on instances, so +# we need to create instances with the read-only properties set to what we need them +# to be. Use this helper function to clone RASDD instances with the specified +# properties and values as given by NewPropertiesDict. Throws if a property that did not +# originally exist on the object is given. +function Copy-CimInstanceWithNewProperties { + param( + [parameter(Mandatory = $true)] + [Microsoft.Management.Infrastructure.CimInstance] $CimInstance, + [parameter(Mandatory = $true)] + [System.Collections.Hashtable] $NewPropertiesDict + ) + + $newProperties = @{ } + + $class = Get-CimClass -Namespace $CimInstance.CimSystemProperties.Namespace ` + -ClassName $CimInstance.CimSystemProperties.ClassName + + $compareArgs = @{ReferenceObject = $class.CimClassProperties.Name; + DifferenceObject = @($NewPropertiesDict.Keys); + PassThru = $true; + CaseSensitive = $false + }; + + $invalidProperties = Compare-Object @compareArgs | Where-Object { $_.SideIndicator -eq "=>" } + if ($invalidProperties) { + throw "Invalid properties are specified - $($invalidProperties -join ',')" + } + + foreach ($prop in $class.CimClassProperties) { + if ($NewPropertiesDict.ContainsKey("$($prop.Name)")) { + $newProperties["$($prop.Name)"] = $NewPropertiesDict["$($prop.Name)"] + } + else { + $newProperties["$($prop.Name)"] = $CimInstance."$($prop.Name)" + } + } + + return ($class | New-CimInstance -ClientOnly -Property $newProperties) +} + +<# +.SYNOPSIS + Helper function that processes a CIMMethodResult/Msvm_ConcreteJob. + +.DESCRIPTION + Helper function that processes a CIMMethodResult/Msvm_ConcreteJob. + +.PARAMETER WmiClass + Supplies the WMI class object from where the method is being called. + +.PARAMETER MethodName + Supplies the method name that the job called. + +.PARAMETER TimeoutSeconds + Supplies the duration in seconds to wait for job completion. + +.INPUTS + Input a CIMMethodResult object through the pipeline, or any object with + a ReturnValue property and optionally a Job property that is an Msvm_ConcreteJob. + +.OUTPUTS + Returns the input object on success; throws on error. + +.EXAMPLE + $job | Trace-CimMethodExecution -WmiClass $VMMS -MethodName ExportSystemDefinition + Processes a job for the given class and method, shows progress until it reaches completion. +#> +filter Trace-CimMethodExecution { + param ( + [Alias("WmiClass")] + [Microsoft.Management.Infrastructure.CimInstance]$CimInstance = $null, + [string] $MethodName = $null, + [int] $TimeoutSeconds = 0 + ) + + $errorCode = 0 + $returnObject = $_ + $job = $null + $shouldProcess = $true + $timer = $null + + if ($_.CimSystemProperties.ClassName -eq "Msvm_ConcreteJob") { + $job = $_ + } + elseif ((Get-Member -InputObject $_ -name "ReturnValue" -MemberType Properties)) { + if ((Get-Member -InputObject $_.ReturnValue -name "Value" -MemberType Properties)) { + # InvokeMethod from New-CimSession return object + $returnValue = $_.ReturnValue.Value + } + else { + # Invoke-CimMethod return object + $returnValue = $_.ReturnValue + } + + if (($returnValue -ne 0) -and ($returnValue -ne 4096)) { + # An error occurred + $errorCode = $returnValue + $shouldProcess = $false + } + elseif ($returnValue -eq 4096) { + if ((Get-Member -InputObject $_ -name "Job" -MemberType Properties) -and $_.Job) { + # Invoke-CimMethod return object + # CIM does not seem to actually populate the non-key fields on a reference, so we need + # to go get the actual instance of the job object we got. + $job = ($_.Job | Get-CimInstance) + } + elseif ((Get-Member -InputObject $_ -name "OutParameters" -MemberType Properties) -and $_.OutParameters["Job"]) { + # InvokeMethod from New-CimSession return object + $job = ($_.OutParameters["Job"].Value | Get-CimInstance) + } + else { + throw "ReturnValue of 4096 with no Job object!" + } + } + else { + # No job and no error, just exit. + return $returnObject + } + } + else { + throw "Pipeline input object is not a job or CIM method result!" + } + + if ($shouldProcess) { + $caption = if ($job.Caption) { $job.Caption } else { "Job in progress (no caption available)" } + $jobStatus = if ($job.JobStatus) { $job.JobState } else { "No job status available" } + $percentComplete = if ($job.PercentComplete) { $job.PercentComplete } else { 0 } + + if (($job.JobState -eq 4) -and $TimeoutSeconds -gt 0) { + $timer = [Diagnostics.Stopwatch]::StartNew() + } + + while ($job.JobState -eq 4) { + if (($timer -ne $null) -and ($timer.Elapsed.TotalSeconds -gt $TimeoutSeconds)) { + throw "Job did not complete within $TimeoutSeconds seconds!" + } + Write-Progress -Activity $caption -Status ("{0} - {1}%" -f $jobStatus, $percentComplete) -PercentComplete $percentComplete + Start-Sleep -seconds 1 + $job = $job | Get-CimInstance + } + + if ($timer) { $timer.Stop() } + + if ($job.JobState -ne 7) { + if (![string]::IsNullOrEmpty($job.ErrorDescription)) { + Throw $job.ErrorDescription + } + else { + $errorCode = $job.ErrorCode + } + } + Write-Progress -Activity $caption -Status $jobStatus -PercentComplete 100 -Completed:$true + } + + if ($errorCode -ne 0) { + if ($CimInstance -and $MethodName) { + $cimClass = Get-CimClass -ClassName $CimInstance.CimSystemProperties.ClassName ` + -Namespace $CimInstance.CimSystemProperties.Namespace -ComputerName $CimInstance.CimSystemProperties.ServerName + + $methodQualifierValues = ($cimClass.CimClassMethods[$MethodName].Qualifiers["ValueMap"].Value) + $indexOfError = [System.Array]::IndexOf($methodQualifierValues, [string]$errorCode) + + if (($indexOfError -ne "-1") -and $methodQualifierValues) { + # If the class in question has an error description defined for the error in its Values collection, use it + if ($cimClass.CimClassMethods[$MethodName].Qualifiers["Values"] -and $indexOfError -lt $cimClass.CimClassMethods[$MethodName].Qualifiers["Values"].Value.Length) { + Throw "ReturnCode: ", $errorCode, " ErrorMessage: '", $cimClass.CimClassMethods[$MethodName].Qualifiers["Values"].Value[$indexOfError], "' - when calling $MethodName" + } + else { + # The class has no error description for the error code, so just return the error code + Throw "ReturnCode: ", $errorCode, " - when calling $MethodName" + } + } + else { + # The error code is not found in the ValueMap, so just return the error code + Throw "ReturnCode: ", $errorCode, " ErrorMessage: 'MessageNotFound' - when calling $MethodName" + } + } + else { + Throw "ReturnCode: ", $errorCode, "When calling $MethodName - for rich error messages provide classpath and method name." + } + } + + return $returnObject +} + +<# +.SYNOPSIS + Get the __PATH property from a CIMInstance object. + +.DESCRIPTION + The Get-CIMInstance cmdlet by default doesn't display the WMI system properties + like __SERVER. The properties are available in the CimSystemProperties property + except for __PATH. This function will construct the __PATH property and return it. + +.EXAMPLE + get-ciminstance win32_memorydevice | get-ciminstancepath + + \\SERVER01\root\cimv2:Win32_MemoryDevice.DeviceID="Memory Device 0" + \\SERVER01\root\cimv2:Win32_MemoryDevice.DeviceID="Memory Device 1" + +.INPUTS + A CIMInstance object + +.OUTPUTS + String representing the path of the input object +#> +function Get-CimInstancePath { + [CmdletBinding()] + param ( + [Parameter(Position = 0, ValueFromPipeline = $True)] + [ValidateNotNullorEmpty()] + [Microsoft.Management.Infrastructure.CimInstance]$CimInstance + ) + + $key = $CimInstance.CimClass.CimClassProperties | + Where-Object { $_.Qualifiers.Name -contains "key" } | + Select-Object -ExpandProperty Name + + $path = ('\\{0}\{1}:{2}{3}' -f $CimInstance.CimSystemProperties.ServerName.ToUpper(), + $CimInstance.CimSystemProperties.Namespace.Replace("/", "\"), + $CimInstance.CimSystemProperties.ClassName, + $(if ($key -is [array]) { + # Need a string with every key in the array, keys separated by commas + $sep = "" + $s = [string]"." + foreach ($k in $key) { + $s += "$($sep)$($k)=""$($CimInstance.($k))""" + $sep = "," + } + $s + } + elseif ($key) { + # just a single key + ".$($key)=""$($CimInstance.$key)""" + } + else { + #no key + '=@' + }).Replace('\', '\\') + ) + + return $path +} diff --git a/petri/src/vm/hyperv/hyperv.psm1 b/petri/src/vm/hyperv/hyperv.psm1 index 486e3fadb1..2c8b43fea1 100644 --- a/petri/src/vm/hyperv/hyperv.psm1 +++ b/petri/src/vm/hyperv/hyperv.psm1 @@ -1,6 +1,8 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +Import-Module Utilities + # # Constants # @@ -17,43 +19,6 @@ $DVD_DISK_TYPE = "Microsoft:Hyper-V:Virtual CD/DVD Disk" # Hyper-V Helpers # -function Get-MsvmComputerSystem -{ - [CmdletBinding()] - Param ( - [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] - [System.Object] - $Vm - ) - - $vmid = $Vm.Id - $msvmComputerSystem = Get-CimInstance -namespace $ROOT_HYPER_V_NAMESPACE -query "select * from Msvm_ComputerSystem where Name = '$vmid'" - - if (-not $msvmComputerSystem) - { - throw "Unable to find a virtual machine with id $vmid." - } - - $msvmComputerSystem -} - -function Get-VmSystemSettings -{ - [CmdletBinding()] - Param ( - [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] - [System.Object] - $Vm - ) - - Get-MsvmComputerSystem $Vm | Get-CimAssociatedInstance -ResultClass "Msvm_VirtualSystemSettingData" -Association "Msvm_SettingsDefineState" -} - -function Get-Vmms -{ - Get-CimInstance -Namespace $ROOT_HYPER_V_NAMESPACE -Class Msvm_VirtualSystemManagementService -} - function Get-VmGuestManagementService { Get-CimInstance -Namespace $ROOT_HYPER_V_NAMESPACE -Class Msvm_VirtualSystemGuestManagementService @@ -85,23 +50,6 @@ function Set-VmResourceSettings { } | Trace-CimMethodExecution -MethodName "ModifyResourceSettings" -CimInstance $vmms } -function Add-VmResourceSettings { - param( - [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] - [System.Object] $Vm, - - [Parameter(Mandatory = $true)] - [Microsoft.Management.Infrastructure.CimInstance] $Rasd - ) - - $vssd = Get-VmSystemSettings $Vm - $vmms = Get-Vmms - $vmms | Invoke-CimMethod -Name "AddResourceSettings" -Arguments @{ - "AffectedConfiguration" = $vssd; - "ResourceSettings" = @($Rasd | ConvertTo-CimEmbeddedString) - } | Trace-CimMethodExecution -MethodName "AddResourceSettings" -CimInstance $vmms -} - function Remove-VmResourceSettings { param( [Parameter(Position = 0, Mandatory = $true, ValueFromPipeline = $true)] @@ -237,6 +185,16 @@ function New-CustomVM # by argument order. [hashtable] $NvmeControllers = $null, + # must be a hashtable with format: + # PhysicalNvmeControllers => { + # Vsid => { + # Vtl, + # Nsid + # }, + # ... + # } + [hashtable] $PhysicalNvmeControllers = $null, + # must be a hashtable with format: # IdeControllers => { # ControllerNumber => { @@ -431,6 +389,17 @@ function New-CustomVM } | Trace-CimMethodExecution -MethodName "AddResourceSettings" -CimInstance $vmms | Out-Null } + # Assign physical NVMe devices via PhysicalNvme module + if ($PhysicalNvmeControllers) { + Import-Module PhysicalNvme + foreach ($entry in $PhysicalNvmeControllers.GetEnumerator()) { + $vsid = $entry.Name + $targetVtl = $entry.Value["Vtl"] + $nsid = $entry.Value["Nsid"] + Add-PhysicalNvmeDeviceToVm -VmName $VMName -Vsid $vsid -Nsid $nsid -TargetVtl $targetVtl | Out-Null + } + } + if ($Com1 -or $Com3) { $serialPorts = $vssd | Get-CimAssociatedInstance -ResultClassName "Msvm_SerialPortSettingData" $resourceSettings = @() @@ -1187,270 +1156,3 @@ function Set-GuestStateIsolationMode $vssd.GuestStateIsolationMode = $Mode Set-VmSystemSettings $vssd } - -# -# CIM Helpers -# - -function ConvertTo-CimEmbeddedString -{ - [CmdletBinding()] - param( - [Parameter(ValueFromPipeline)] - [Microsoft.Management.Infrastructure.CimInstance] $CimInstance - ) - - if ($null -eq $CimInstance) - { - return "" - } - - $cimSerializer = [Microsoft.Management.Infrastructure.Serialization.CimSerializer]::Create() - $serializedObj = $cimSerializer.Serialize($CimInstance, [Microsoft.Management.Infrastructure.Serialization.InstanceSerializationOptions]::None) - return [System.Text.Encoding]::Unicode.GetString($serializedObj) -} - -# CIM is strict and won't let you write read-only properties on instances, so -# we need to create instances with the read-only properties set to what we need them -# to be. Use this helper function to clone RASDD instances with the specified -# properties and values as given by NewPropertiesDict. Throws if a property that did not -# originally exist on the object is given. -function Copy-CimInstanceWithNewProperties { - param( - [parameter(Mandatory = $true)] - [Microsoft.Management.Infrastructure.CimInstance] $CimInstance, - [parameter(Mandatory = $true)] - [System.Collections.Hashtable] $NewPropertiesDict - ) - - $newProperties = @{ } - - $class = Get-CimClass -Namespace $CimInstance.CimSystemProperties.Namespace ` - -ClassName $CimInstance.CimSystemProperties.ClassName - - $compareArgs = @{ReferenceObject = $class.CimClassProperties.Name; - DifferenceObject = @($NewPropertiesDict.Keys); - PassThru = $true; - CaseSensitive = $false - }; - - $invalidProperties = Compare-Object @compareArgs | Where-Object { $_.SideIndicator -eq "=>" } - if ($invalidProperties) { - throw "Invalid properties are specified - $($invalidProperties -join ',')" - } - - foreach ($prop in $class.CimClassProperties) { - if ($NewPropertiesDict.ContainsKey("$($prop.Name)")) { - $newProperties["$($prop.Name)"] = $NewPropertiesDict["$($prop.Name)"] - } - else { - $newProperties["$($prop.Name)"] = $CimInstance."$($prop.Name)" - } - } - - return ($class | New-CimInstance -ClientOnly -Property $newProperties) -} - -<# -.SYNOPSIS - Helper function that processes a CIMMethodResult/Msvm_ConcreteJob. - -.DESCRIPTION - Helper function that processes a CIMMethodResult/Msvm_ConcreteJob. - -.PARAMETER WmiClass - Supplies the WMI class object from where the method is being called. - -.PARAMETER MethodName - Supplies the method name that the job called. - -.PARAMETER TimeoutSeconds - Supplies the duration in seconds to wait for job completion. - -.INPUTS - Input a CIMMethodResult object through the pipeline, or any object with - a ReturnValue property and optionally a Job property that is an Msvm_ConcreteJob. - -.OUTPUTS - Returns the input object on success; throws on error. - -.EXAMPLE - $job | Trace-CimMethodExecution -WmiClass $VMMS -MethodName ExportSystemDefinition - Processes a job for the given class and method, shows progress until it reaches completion. -#> -filter Trace-CimMethodExecution { - param ( - [Alias("WmiClass")] - [Microsoft.Management.Infrastructure.CimInstance]$CimInstance = $null, - [string] $MethodName = $null, - [int] $TimeoutSeconds = 0 - ) - - $errorCode = 0 - $returnObject = $_ - $job = $null - $shouldProcess = $true - $timer = $null - - if ($_.CimSystemProperties.ClassName -eq "Msvm_ConcreteJob") { - $job = $_ - } - elseif ((Get-Member -InputObject $_ -name "ReturnValue" -MemberType Properties)) { - if ((Get-Member -InputObject $_.ReturnValue -name "Value" -MemberType Properties)) { - # InvokeMethod from New-CimSession return object - $returnValue = $_.ReturnValue.Value - } - else { - # Invoke-CimMethod return object - $returnValue = $_.ReturnValue - } - - if (($returnValue -ne 0) -and ($returnValue -ne 4096)) { - # An error occurred - $errorCode = $returnValue - $shouldProcess = $false - } - elseif ($returnValue -eq 4096) { - if ((Get-Member -InputObject $_ -name "Job" -MemberType Properties) -and $_.Job) { - # Invoke-CimMethod return object - # CIM does not seem to actually populate the non-key fields on a reference, so we need - # to go get the actual instance of the job object we got. - $job = ($_.Job | Get-CimInstance) - } - elseif ((Get-Member -InputObject $_ -name "OutParameters" -MemberType Properties) -and $_.OutParameters["Job"]) { - # InvokeMethod from New-CimSession return object - $job = ($_.OutParameters["Job"].Value | Get-CimInstance) - } - else { - throw "ReturnValue of 4096 with no Job object!" - } - } - else { - # No job and no error, just exit. - return $returnObject - } - } - else { - throw "Pipeline input object is not a job or CIM method result!" - } - - if ($shouldProcess) { - $caption = if ($job.Caption) { $job.Caption } else { "Job in progress (no caption available)" } - $jobStatus = if ($job.JobStatus) { $job.JobState } else { "No job status available" } - $percentComplete = if ($job.PercentComplete) { $job.PercentComplete } else { 0 } - - if (($job.JobState -eq 4) -and $TimeoutSeconds -gt 0) { - $timer = [Diagnostics.Stopwatch]::StartNew() - } - - while ($job.JobState -eq 4) { - if (($timer -ne $null) -and ($timer.Elapsed.TotalSeconds -gt $TimeoutSeconds)) { - throw "Job did not complete within $TimeoutSeconds seconds!" - } - Write-Progress -Activity $caption -Status ("{0} - {1}%" -f $jobStatus, $percentComplete) -PercentComplete $percentComplete - Start-Sleep -seconds 1 - $job = $job | Get-CimInstance - } - - if ($timer) { $timer.Stop() } - - if ($job.JobState -ne 7) { - if (![string]::IsNullOrEmpty($job.ErrorDescription)) { - Throw $job.ErrorDescription - } - else { - $errorCode = $job.ErrorCode - } - } - Write-Progress -Activity $caption -Status $jobStatus -PercentComplete 100 -Completed:$true - } - - if ($errorCode -ne 0) { - if ($CimInstance -and $MethodName) { - $cimClass = Get-CimClass -ClassName $CimInstance.CimSystemProperties.ClassName ` - -Namespace $CimInstance.CimSystemProperties.Namespace -ComputerName $CimInstance.CimSystemProperties.ServerName - - $methodQualifierValues = ($cimClass.CimClassMethods[$MethodName].Qualifiers["ValueMap"].Value) - $indexOfError = [System.Array]::IndexOf($methodQualifierValues, [string]$errorCode) - - if (($indexOfError -ne "-1") -and $methodQualifierValues) { - # If the class in question has an error description defined for the error in its Values collection, use it - if ($cimClass.CimClassMethods[$MethodName].Qualifiers["Values"] -and $indexOfError -lt $cimClass.CimClassMethods[$MethodName].Qualifiers["Values"].Value.Length) { - Throw "ReturnCode: ", $errorCode, " ErrorMessage: '", $cimClass.CimClassMethods[$MethodName].Qualifiers["Values"].Value[$indexOfError], "' - when calling $MethodName" - } - else { - # The class has no error description for the error code, so just return the error code - Throw "ReturnCode: ", $errorCode, " - when calling $MethodName" - } - } - else { - # The error code is not found in the ValueMap, so just return the error code - Throw "ReturnCode: ", $errorCode, " ErrorMessage: 'MessageNotFound' - when calling $MethodName" - } - } - else { - Throw "ReturnCode: ", $errorCode, "When calling $MethodName - for rich error messages provide classpath and method name." - } - } - - return $returnObject -} - -<# -.SYNOPSIS - Get the __PATH property from a CIMInstance object. - -.DESCRIPTION - The Get-CIMInstance cmdlet by default doesn't display the WMI system properties - like __SERVER. The properties are available in the CimSystemProperties property - except for __PATH. This function will construct the __PATH property and return it. - -.EXAMPLE - get-ciminstance win32_memorydevice | get-ciminstancepath - - \\SERVER01\root\cimv2:Win32_MemoryDevice.DeviceID="Memory Device 0" - \\SERVER01\root\cimv2:Win32_MemoryDevice.DeviceID="Memory Device 1" - -.INPUTS - A CIMInstance object - -.OUTPUTS - String representing the path of the input object -#> -function Get-CimInstancePath { - [CmdletBinding()] - param ( - [Parameter(Position = 0, ValueFromPipeline = $True)] - [ValidateNotNullorEmpty()] - [Microsoft.Management.Infrastructure.CimInstance]$CimInstance - ) - - $key = $CimInstance.CimClass.CimClassProperties | - Where-Object { $_.Qualifiers.Name -contains "key" } | - Select-Object -ExpandProperty Name - - $path = ('\\{0}\{1}:{2}{3}' -f $CimInstance.CimSystemProperties.ServerName.ToUpper(), - $CimInstance.CimSystemProperties.Namespace.Replace("/", "\"), - $CimInstance.CimSystemProperties.ClassName, - $(if ($key -is [array]) { - # Need a string with every key in the array, keys separated by commas - $sep = "" - $s = [string]"." - foreach ($k in $key) { - $s += "$($sep)$($k)=""$($CimInstance.($k))""" - $sep = "," - } - $s - } - elseif ($key) { - # just a single key - ".$($key)=""$($CimInstance.$key)""" - } - else { - #no key - '=@' - }).Replace('\', '\\') - ) - - return $path -} diff --git a/petri/src/vm/hyperv/mod.rs b/petri/src/vm/hyperv/mod.rs index 6395e8323e..3effd56e58 100644 --- a/petri/src/vm/hyperv/mod.rs +++ b/petri/src/vm/hyperv/mod.rs @@ -4,6 +4,7 @@ mod hvc; pub mod powershell; pub mod vm; + use vmsocket::VmAddress; use vmsocket::VmSocket; @@ -368,6 +369,7 @@ impl PetriVmmBackend for HyperVPetriBackend { com_3: supports_com3, imc_hiv, management_vtl_settings, + ..HyperVNewCustomVMArgs::from_config(&config, &properties)? }; diff --git a/petri/src/vm/hyperv/powershell.rs b/petri/src/vm/hyperv/powershell.rs index 5e565b9840..d127ac35ad 100644 --- a/petri/src/vm/hyperv/powershell.rs +++ b/petri/src/vm/hyperv/powershell.rs @@ -303,6 +303,8 @@ pub struct HyperVNewCustomVMArgs { pub tpm_enabled: bool, /// Temporary file containing management VTL settings pub management_vtl_settings: Option, + /// Physical NVMe devices, used exclusively in closed source tests + pub physical_nvme_devices: Vec, } /// VMBus storage controller type @@ -458,6 +460,7 @@ impl HyperVNewCustomVMArgs { proc_topology, vmgs, tpm, + physical_nvme_devices, .. } = config; @@ -579,6 +582,7 @@ impl HyperVNewCustomVMArgs { com_3: false, imc_hiv: None, management_vtl_settings: None, + physical_nvme_devices: physical_nvme_devices.clone(), }) } } @@ -718,6 +722,22 @@ pub async fn run_new_customvm(ps_mod: &Path, args: HyperVNewCustomVMArgs) -> any Some(ps::HashTable::new(nvme_entries)) }; + let physical_nvme_controllers = if args.physical_nvme_devices.is_empty() { + None + } else { + Some(ps::HashTable::new(args.physical_nvme_devices.iter().map( + |dev| { + ( + format!("\"{}\"", dev.vsid), + ps::Value::new(ps::HashTable::new([ + ("Vtl", ps::Value::new(dev.target_vtl as u32)), + ("Nsid", ps::Value::new(dev.nsid)), + ])), + ) + }, + ))) + }; + let builder = PowerShellBuilder::new() .cmdlet("Import-Module") .positional(ps_mod) @@ -762,6 +782,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("PhysicalNvmeControllers", physical_nvme_controllers) .arg_opt("ImcHive", args.imc_hiv.as_ref().map(|f| f.path())) .arg("Com1", args.com_1) .arg("Com3", args.com_3) @@ -796,6 +817,38 @@ pub async fn run_remove_vm(vmid: &Guid) -> anyhow::Result<()> { .context("remove_vm") } +/// Request a physical NVMe device via closed-source script +pub async fn request_physical_nvme( + namespace_size_mib: u64, + target_vtl: crate::Vtl, +) -> anyhow::Result { + let output = run_host_cmd( + PowerShellBuilder::new() + .cmdlet("Import-Module") + .positional("PhysicalNvme") + .next() + .cmdlet("Get-PhysicalNvmeDevices") + .arg("Count", 1u64) + .arg("NamespaceSizeMiB", namespace_size_mib) + .finish() + .build(), + ) + .await + .context("physical NVMe device discovery")?; + + let nsid: u32 = + serde_json::from_str(&output).context("failed to parse PhysicalNvme discovery output")?; + + let vsid = Guid::new_random(); + + Ok(crate::PhysicalNvmeDevice { + target_vtl, + vsid, + nsid, + namespace_size_mib, + }) +} + /// Arguments for the Set-VMProcessor powershell cmdlet pub struct HyperVSetVMProcessorArgs { /// Specifies the number of virtual processors to assign to the virtual diff --git a/petri/src/vm/mod.rs b/petri/src/vm/mod.rs index 661e50827d..79d1a14938 100644 --- a/petri/src/vm/mod.rs +++ b/petri/src/vm/mod.rs @@ -228,6 +228,8 @@ pub struct PetriVmConfig { pub vmbus_storage_controllers: HashMap, /// PCIe NVMe drives. pub pcie_nvme_drives: Vec, + /// Physical NVMe devices to attach. + pub physical_nvme_devices: Vec, } /// PCIe NVMe drive configuration. @@ -241,6 +243,20 @@ pub struct PcieNvmeDrive { pub drive: Drive, } +/// Physical NVMe device to assign to a VM. +/// Only used in closed-source HyperV tests +#[derive(Debug, Clone)] +pub struct PhysicalNvmeDevice { + /// The VTL to assign the physical NVMe device to. + pub target_vtl: Vtl, + /// VSID for the device. + pub vsid: Guid, + /// NVMe namespace ID. + pub nsid: u32, + /// Namespace size in MiB + pub namespace_size_mib: u64, +} + /// Static properties about the VM for convenience during contruction and /// runtime of a VMM backend pub struct PetriVmProperties { @@ -412,6 +428,7 @@ impl PetriVmBuilder { tpm: None, vmbus_storage_controllers: HashMap::new(), pcie_nvme_drives: Vec::new(), + physical_nvme_devices: Vec::new(), }, modify_vmm_config: None, resources: PetriVmResources { @@ -485,6 +502,7 @@ impl PetriVmBuilder { tpm: None, vmbus_storage_controllers: HashMap::new(), pcie_nvme_drives: Vec::new(), + physical_nvme_devices: Vec::new(), }, modify_vmm_config: None, resources: PetriVmResources { @@ -1492,6 +1510,12 @@ impl PetriVmBuilder { self } + /// Add a physical NVMe device to the VM. + pub fn add_physical_nvme_device(mut self, device: PhysicalNvmeDevice) -> Self { + self.config.physical_nvme_devices.push(device); + self + } + /// Get VM's guest OS flavor pub fn os_flavor(&self) -> OsFlavor { self.config.firmware.os_flavor() diff --git a/petri/src/vm/openvmm/construct.rs b/petri/src/vm/openvmm/construct.rs index 271a1c4859..7e5d534126 100644 --- a/petri/src/vm/openvmm/construct.rs +++ b/petri/src/vm/openvmm/construct.rs @@ -121,8 +121,13 @@ impl PetriVmConfigOpenVmm { tpm: tpm_config, vmbus_storage_controllers, pcie_nvme_drives, + physical_nvme_devices, } = petri_vm_config; + if !physical_nvme_devices.is_empty() { + anyhow::bail!("Physical NVMe devices are only supported with the Hyper-V backend"); + } + tracing::debug!(?firmware, ?arch, "Petri VM firmware configuration"); let PetriVmResources { driver, log_source } = resources; diff --git a/vmm_tests/vmm_tests/tests/tests/x86_64/storage.rs b/vmm_tests/vmm_tests/tests/tests/x86_64/storage.rs index cf5500cf21..0acdab198c 100644 --- a/vmm_tests/vmm_tests/tests/tests/x86_64/storage.rs +++ b/vmm_tests/vmm_tests/tests/tests/x86_64/storage.rs @@ -449,6 +449,7 @@ async fn storvsp_nvme_hyperv( Ok(()) } + #[openvmm_test( openhcl_linux_direct_x64, openhcl_uefi_x64(vhd(ubuntu_2504_server_x64)) From e13986ecb5ab3dede71f362f61c6cc45d9d4b55e Mon Sep 17 00:00:00 2001 From: Hadi Orabi Date: Mon, 18 May 2026 05:05:47 +0000 Subject: [PATCH 2/5] fmt fix --- petri/src/vm/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/petri/src/vm/mod.rs b/petri/src/vm/mod.rs index 79d1a14938..940b27abe4 100644 --- a/petri/src/vm/mod.rs +++ b/petri/src/vm/mod.rs @@ -253,7 +253,7 @@ pub struct PhysicalNvmeDevice { pub vsid: Guid, /// NVMe namespace ID. pub nsid: u32, - /// Namespace size in MiB + /// Namespace size in MiB pub namespace_size_mib: u64, } From 8186c0ba94438cd536240c02b29d6f6d9399fb91 Mon Sep 17 00:00:00 2001 From: Hadi Orabi Date: Mon, 18 May 2026 19:18:53 +0000 Subject: [PATCH 3/5] copy utilities.psm1 --- petri/src/vm/hyperv/hyperv.psm1 | 2 +- petri/src/vm/hyperv/vm.rs | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/petri/src/vm/hyperv/hyperv.psm1 b/petri/src/vm/hyperv/hyperv.psm1 index 2c8b43fea1..6e1196cd9a 100644 --- a/petri/src/vm/hyperv/hyperv.psm1 +++ b/petri/src/vm/hyperv/hyperv.psm1 @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -Import-Module Utilities + Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "Utilities.psm1") # # Constants diff --git a/petri/src/vm/hyperv/vm.rs b/petri/src/vm/hyperv/vm.rs index 86ed362cef..4a33aeae88 100644 --- a/petri/src/vm/hyperv/vm.rs +++ b/petri/src/vm/hyperv/vm.rs @@ -67,7 +67,13 @@ impl HyperVVM { let mut ps_mod_file = std::fs::File::create_new(&ps_mod)?; ps_mod_file .write_all(include_bytes!("hyperv.psm1")) - .context("failed to write hyperv helpers powershell module")?; + .context("failed to write hyperv powershell module")?; + + let mut utilities_mod_file = + std::fs::File::create_new(temp_dir.path().join("Utilities.psm1"))?; + utilities_mod_file + .write_all(include_bytes!("Utilities.psm1")) + .context("failed to write hyperv utilities powershell module")?; } // Used to ignore `hvc restart` error on CVMs From 1cfb62be43305654dd0906c3fb3ffd7af8b327d1 Mon Sep 17 00:00:00 2001 From: Hadi Orabi Date: Thu, 21 May 2026 07:08:48 +0000 Subject: [PATCH 4/5] feedback from Trevor --- petri/src/vm/hyperv/hyperv.psm1 | 24 +++++++++---------- petri/src/vm/hyperv/mod.rs | 1 + petri/src/vm/hyperv/powershell.rs | 4 ++-- .../hyperv/{Utilities.psm1 => utilities.psm1} | 0 petri/src/vm/hyperv/vm.rs | 4 ++-- 5 files changed, 17 insertions(+), 16 deletions(-) rename petri/src/vm/hyperv/{Utilities.psm1 => utilities.psm1} (100%) diff --git a/petri/src/vm/hyperv/hyperv.psm1 b/petri/src/vm/hyperv/hyperv.psm1 index 6e1196cd9a..f29ddd0af1 100644 --- a/petri/src/vm/hyperv/hyperv.psm1 +++ b/petri/src/vm/hyperv/hyperv.psm1 @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "Utilities.psm1") + Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "utilities.psm1") # # Constants @@ -345,6 +345,17 @@ function New-CustomVM } } + # Assign physical NVMe devices via PhysicalNvme module + if ($PhysicalNvmeControllers) { + Import-Module PhysicalNvme -ErrorAction Stop + foreach ($entry in $PhysicalNvmeControllers.GetEnumerator()) { + $vsid = $entry.Name + $targetVtl = $entry.Value["Vtl"] + $nsid = $entry.Value["Nsid"] + $resourceSettings += Get-PhysicalNvmeDeviceRasd -Vsid $vsid -Nsid $nsid -TargetVtl $targetVtl + } + } + $vm = ($vmms | Invoke-CimMethod -Name "DefineSystem" -Arguments @{ "SystemSettings" = ($vssd | ConvertTo-CimEmbeddedString); "ResourceSettings" = $resourceSettings @@ -389,17 +400,6 @@ function New-CustomVM } | Trace-CimMethodExecution -MethodName "AddResourceSettings" -CimInstance $vmms | Out-Null } - # Assign physical NVMe devices via PhysicalNvme module - if ($PhysicalNvmeControllers) { - Import-Module PhysicalNvme - foreach ($entry in $PhysicalNvmeControllers.GetEnumerator()) { - $vsid = $entry.Name - $targetVtl = $entry.Value["Vtl"] - $nsid = $entry.Value["Nsid"] - Add-PhysicalNvmeDeviceToVm -VmName $VMName -Vsid $vsid -Nsid $nsid -TargetVtl $targetVtl | Out-Null - } - } - if ($Com1 -or $Com3) { $serialPorts = $vssd | Get-CimAssociatedInstance -ResultClassName "Msvm_SerialPortSettingData" $resourceSettings = @() diff --git a/petri/src/vm/hyperv/mod.rs b/petri/src/vm/hyperv/mod.rs index 3effd56e58..cf0f39f789 100644 --- a/petri/src/vm/hyperv/mod.rs +++ b/petri/src/vm/hyperv/mod.rs @@ -49,6 +49,7 @@ use petri_artifacts_common::tags::OsFlavor; use petri_artifacts_core::ArtifactResolver; use petri_artifacts_core::ResolvedArtifact; use pipette_client::PipetteClient; +pub use powershell::add_physical_nvme_device; use std::collections::HashMap; use std::io::ErrorKind; use std::io::Write; diff --git a/petri/src/vm/hyperv/powershell.rs b/petri/src/vm/hyperv/powershell.rs index d127ac35ad..cb205e538c 100644 --- a/petri/src/vm/hyperv/powershell.rs +++ b/petri/src/vm/hyperv/powershell.rs @@ -293,6 +293,8 @@ pub struct HyperVNewCustomVMArgs { pub storage_controllers: HashMap, /// IDE controllers and associated drives/disks pub ide_controllers: HashMap>, + /// Physical NVMe devices, used exclusively in closed source tests + pub physical_nvme_devices: Vec, /// Temporary file containing initial machine configuration data pub imc_hiv: Option, /// Enable COM1 at \\.\pipe\-1 @@ -303,8 +305,6 @@ pub struct HyperVNewCustomVMArgs { pub tpm_enabled: bool, /// Temporary file containing management VTL settings pub management_vtl_settings: Option, - /// Physical NVMe devices, used exclusively in closed source tests - pub physical_nvme_devices: Vec, } /// VMBus storage controller type diff --git a/petri/src/vm/hyperv/Utilities.psm1 b/petri/src/vm/hyperv/utilities.psm1 similarity index 100% rename from petri/src/vm/hyperv/Utilities.psm1 rename to petri/src/vm/hyperv/utilities.psm1 diff --git a/petri/src/vm/hyperv/vm.rs b/petri/src/vm/hyperv/vm.rs index 4a33aeae88..6c95d481ce 100644 --- a/petri/src/vm/hyperv/vm.rs +++ b/petri/src/vm/hyperv/vm.rs @@ -70,9 +70,9 @@ impl HyperVVM { .context("failed to write hyperv powershell module")?; let mut utilities_mod_file = - std::fs::File::create_new(temp_dir.path().join("Utilities.psm1"))?; + std::fs::File::create_new(temp_dir.path().join("utilities.psm1"))?; utilities_mod_file - .write_all(include_bytes!("Utilities.psm1")) + .write_all(include_bytes!("utilities.psm1")) .context("failed to write hyperv utilities powershell module")?; } From 0d639243e94a42f4c8ce42140b588d4121804785 Mon Sep 17 00:00:00 2001 From: Hadi Orabi Date: Thu, 21 May 2026 07:19:56 +0000 Subject: [PATCH 5/5] copilot feedback --- petri/src/vm/hyperv/hyperv.psm1 | 2 +- petri/src/vm/hyperv/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/petri/src/vm/hyperv/hyperv.psm1 b/petri/src/vm/hyperv/hyperv.psm1 index f29ddd0af1..0d36fcc211 100644 --- a/petri/src/vm/hyperv/hyperv.psm1 +++ b/petri/src/vm/hyperv/hyperv.psm1 @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. - Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "utilities.psm1") + Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "utilities.psm1") -ErrorAction Stop # # Constants diff --git a/petri/src/vm/hyperv/mod.rs b/petri/src/vm/hyperv/mod.rs index cf0f39f789..fbf2df7b00 100644 --- a/petri/src/vm/hyperv/mod.rs +++ b/petri/src/vm/hyperv/mod.rs @@ -49,7 +49,7 @@ use petri_artifacts_common::tags::OsFlavor; use petri_artifacts_core::ArtifactResolver; use petri_artifacts_core::ResolvedArtifact; use pipette_client::PipetteClient; -pub use powershell::add_physical_nvme_device; +pub use powershell::request_physical_nvme; use std::collections::HashMap; use std::io::ErrorKind; use std::io::Write;