From 3812f9fd9b2d0711cbe800ddcc63ae5f8586d53b Mon Sep 17 00:00:00 2001 From: Adam Rudell Date: Thu, 13 Aug 2026 14:33:42 -0500 Subject: [PATCH 1/6] Add CIM-based VM/adapter/switch enumeration for faster performance Replace slow Hyper-V cmdlets (Get-VM, Get-VMNetworkAdapter) with direct CIM queries against root/virtualization/v2 for significantly faster enumeration on hosts with large numbers of VMs. New functions: - Get-SdnVMCim: CIM-based VM enumeration - Get-SdnVMNetworkAdapterCim: CIM-based adapter enumeration with -All, -ManagementOS, -VMName, -MacAddress support - Get-SdnVMSwitchCim: CIM-based virtual switch enumeration Centralized CIM session management: - New-SdnCimSession/Remove-SdnCimSession in Utilities, mirroring the existing PSRemotingSession pattern Updated callers: - Get-ServerConfigState uses CIM methods - Get/Set-SdnVMNetworkAdapterPortProfile rewritten to use CIM - ComputerName/Credential params added to Get-SdnVMNetworkAdapterPortProfile Closes #404 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 220ab25a-0e38-4217-8e3c-e0a2e22fd7d9 --- src/modules/SdnDiag.Server.psm1 | 764 ++++++++++++++++++++++++++--- src/modules/SdnDiag.Utilities.psm1 | 120 +++++ 2 files changed, 817 insertions(+), 67 deletions(-) diff --git a/src/modules/SdnDiag.Server.psm1 b/src/modules/SdnDiag.Server.psm1 index 4d08437d..8a14ff98 100644 --- a/src/modules/SdnDiag.Server.psm1 +++ b/src/modules/SdnDiag.Server.psm1 @@ -489,9 +489,9 @@ function Get-ServerConfigState { } } - # Gather Hyper-V network details - "Gathering Hyper-V VM and VMNetworkAdapter configuration details" | Trace-Output -Level:Verbose - $virtualMachines = Get-VM + # Gather Hyper-V network details using CIM for faster enumeration on hosts with large VM counts + "Gathering Hyper-V VM and VMNetworkAdapter configuration details via CIM" | Trace-Output -Level:Verbose + $virtualMachines = Get-SdnVMCim if ($virtualMachines) { $virtualMachines | Export-ObjectToFile -FilePath $outDir -Name 'Get-VM' -FileType txt -Format List -Force $virtualMachines | Export-ObjectToFile -FilePath $outDir -Name 'Get-VM' -FileType json @@ -499,26 +499,17 @@ function Get-ServerConfigState { $vmRootDir = New-Item -Path (Join-Path -Path $outDir -ChildPath "VM") -ItemType Directory -Force foreach ($vm in $virtualMachines) { $vmAdapters = $vm.NetworkAdapters - if ($null -eq $vmAdapters) { + if ($null -eq $vmAdapters -or $vmAdapters.Count -eq 0) { continue } $vmNameFormatted = $vm.Name.ToString().Replace(" ", "_").Trim() $vmDir = New-Item -Path (Join-Path -Path $vmRootDir.FullName -ChildPath $vmNameFormatted) -ItemType Directory -Force - # enumerate the VMNetworkAdapters and gather details within the VM properties itself to speed up data processing - # calling each function such as Get-VMNetworkAdapter or Get-VMNetworkAdapterVlan will enumerate the VMNetworkAdapters again and slow down the process foreach ($adapter in $vmAdapters) { try { $prefix = (Format-SdnMacAddress -MacAddress $adapter.MacAddress) - - $adapterModified = $adapter | Remove-PropertiesFromObject -PropertiesToRemove 'AclList','ExtendedAclList','IsolationSetting','RoutingDomainList','VlanSetting','CimSession' - $adapterModified | Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_NetworkAdapter' -FileType txt -Format List - $adapter.AclList | Remove-PropertiesFromObject -PropertiesToRemove 'ParentAdapter' | Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_AclList' -FileType txt -Format List - $adapter.ExtendedAclList | Remove-PropertiesFromObject -PropertiesToRemove 'ParentAdapter','CimSession' | Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_ExtendedAclList' -FileType txt -Format List - $adapter.IsolationSetting | Remove-PropertiesFromObject -PropertiesToRemove 'ParentAdapter','CimSession' | Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_IsolationSetting' -FileType txt -Format List - $adapter.RoutingDomainList | Remove-PropertiesFromObject -PropertiesToRemove 'ParentAdapter','CimSession' | Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_RoutingDomainList' -FileType txt -Format List - $adapter.VlanSetting | Remove-PropertiesFromObject -PropertiesToRemove 'ParentAdapter','CimSession' | Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_VlanSetting' -FileType txt -Format List + $adapter | Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_NetworkAdapter' -FileType txt -Format List } catch { "Failed to enumerate VMNetworkAdapter for {0}" -f $adapter.Name | Trace-Output -Level:Warning @@ -527,16 +518,13 @@ function Get-ServerConfigState { } } - # enumerate the data for all adapters - Get-VMNetworkAdapter -All | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapter_All' -FileType txt -Format List + # enumerate all VM network adapters using CIM + Get-SdnVMNetworkAdapterCim | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapter_All' -FileType txt -Format List Get-SdnVMNetworkAdapterPortProfile -All | Export-ObjectToFile -FilePath $outDir -Name 'Get-SdnVMNetworkAdapterPortProfile_All' -FileType txt -Format List # collect the management OS network adapter details # we do not need this information for general vmnetworkadapters as they are already collected above - Get-VMNetworkAdapterIsolation -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapterIsolation_ManagementOS' -FileType txt -Format List - Get-VMNetworkAdapterTeamMapping -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapterTeamMapping_ManagementOS' -FileType txt -Format List - Get-VMNetworkAdapterVLAN -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapterVLAN _ManagementOS' -FileType txt -Format List - Get-VMNetworkAdapterRoutingDomainMapping -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapterRoutingDomainMapping_ManagementOS' -FileType txt -Format List + Get-SdnVMNetworkAdapterCim -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapter_ManagementOS' -FileType txt -Format List } catch { $_ | Trace-Exception @@ -2600,16 +2588,522 @@ function Get-SdnVMNetworkAdapter { } } + +function Get-SdnVMSwitchCim { + <# + .SYNOPSIS + Retrieves the virtual switches using CIM/WMI for significantly faster performance compared to Get-VMSwitch. + .DESCRIPTION + Uses direct CIM queries against the root/virtualization/v2 namespace to retrieve virtual switch information. + This approach is substantially faster than Get-VMSwitch on hosts with large numbers of VMs. + .PARAMETER Name + Specifies the name of the virtual switch to retrieve. If not specified, all virtual switches are returned. + .PARAMETER ComputerName + Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers. + .PARAMETER Credential + Specifies a user account that has permission to perform this action. The default is the current user. + .PARAMETER CimSession + An existing CIM session to use for the query. When provided, ComputerName and Credential are ignored. + .EXAMPLE + PS> Get-SdnVMSwitchCim + .EXAMPLE + PS> Get-SdnVMSwitchCim -Name 'ConvergedSwitch' + .EXAMPLE + PS> Get-SdnVMSwitchCim -ComputerName 'Server01','Server02' -Credential (Get-Credential) + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [string]$Name, + + [Parameter(Mandatory = $false)] + [System.String[]]$ComputerName, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential = [System.Management.Automation.PSCredential]::Empty, + + [Parameter(Mandatory = $false)] + [Microsoft.Management.Infrastructure.CimSession]$CimSession + ) + + $cimNamespace = 'root/virtualization/v2' + $cimParams = @{ + Namespace = $cimNamespace + ErrorAction = 'Stop' + } + + if ($CimSession) { + $cimParams.Add('CimSession', $CimSession) + } + elseif ($ComputerName) { + $sessionParams = @{ + ComputerName = $ComputerName + } + if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { + $sessionParams.Add('Credential', $Credential) + } + + $CimSession = New-SdnCimSession @sessionParams + $cimParams.Add('CimSession', $CimSession) + } + + try { + $filter = $null + if ($Name) { + $filter = "ElementName = '$Name'" + } + + $switchParams = @{} + $switchParams += $cimParams + if ($filter) { + $switchParams.Add('Filter', $filter) + } + + $switches = Get-CimInstance @switchParams -ClassName 'Msvm_VirtualEthernetSwitch' + $results = [System.Collections.ArrayList]::new() + + foreach ($sw in $switches) { + $switchObject = [PSCustomObject]@{ + Name = $sw.ElementName + SwitchId = $sw.Name + SwitchType = $sw.IOVPreferred + Notes = $sw.Notes + BandwidthPercentage = $sw.MaxIOVOffloads + InstallDate = $sw.InstallDate + } + + [void]$results.Add($switchObject) + } + + return ($results | Sort-Object -Property Name) + } + catch { + $_ | Trace-Exception + $_ | Write-Error + } +} + +function Get-SdnVMNetworkAdapterCim { + <# + .SYNOPSIS + Retrieves the virtual machine network adapters using CIM/WMI for significantly faster performance compared to Get-VMNetworkAdapter. + .DESCRIPTION + Uses direct CIM queries against the root/virtualization/v2 namespace to retrieve virtual machine network adapter information. + This approach is substantially faster than Get-VMNetworkAdapter on hosts with large numbers of VMs. + .PARAMETER VMName + Specifies the name of the virtual machine whose network adapters are to be retrieved. + .PARAMETER MacAddress + Specifies the MAC address of the network adapter to be retrieved. + .PARAMETER All + Switch to indicate to get all virtual machine network interfaces including both VM and Management OS adapters. + .PARAMETER ManagementOS + Specifies the management operating system, i.e. the virtual machine host operating system. + .PARAMETER ComputerName + Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers. + .PARAMETER Credential + Specifies a user account that has permission to perform this action. The default is the current user. + .EXAMPLE + PS> Get-SdnVMNetworkAdapterCim + .EXAMPLE + PS> Get-SdnVMNetworkAdapterCim -VMName 'VM01' + .EXAMPLE + PS> Get-SdnVMNetworkAdapterCim -All + .EXAMPLE + PS> Get-SdnVMNetworkAdapterCim -ComputerName 'Server01','Server02' -Credential (Get-Credential) + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [string]$VMName, + + [Parameter(Mandatory = $false)] + [string]$MacAddress, + + [Parameter(Mandatory = $false)] + [switch]$All, + + [Parameter(Mandatory = $false)] + [switch]$ManagementOS, + + [Parameter(Mandatory = $false)] + [System.String[]]$ComputerName, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential = [System.Management.Automation.PSCredential]::Empty + ) + + $cimNamespace = 'root/virtualization/v2' + $cimParams = @{ + Namespace = $cimNamespace + ErrorAction = 'Stop' + } + + if ($ComputerName) { + $sessionParams = @{ + ComputerName = $ComputerName + } + if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { + $sessionParams.Add('Credential', $Credential) + } + + $cimSession = New-SdnCimSession @sessionParams + $cimParams.Add('CimSession', $cimSession) + } + + try { + $results = [System.Collections.ArrayList]::new() + + # Build a lookup of switch names by their CIM GUID using the dedicated switch function + $switchCimParams = @{} + if ($cimParams.ContainsKey('CimSession')) { + $switchCimParams.Add('CimSession', $cimParams['CimSession']) + } + $switches = Get-SdnVMSwitchCim @switchCimParams + $switchLookup = @{} + foreach ($sw in $switches) { + $switchLookup[$sw.SwitchId] = $sw.Name + } + + # Get port allocations to map adapters to switches + $portAllocations = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetPortAllocationSettingData' + + # Build a lookup: adapter InstanceID -> switch name + $adapterSwitchLookup = @{} + foreach ($port in $portAllocations) { + if ($port.Parent -and $port.HostResource) { + # Parent contains the adapter path, HostResource contains the switch path + # Extract the adapter InstanceID from Parent path + $parentId = $null + if ($port.Parent -match 'Msvm_.*?\.InstanceID="(.+?)"') { + $parentId = $Matches[1] -replace '\\\\', '\' + } + + # Extract the switch GUID from HostResource + $switchName = $null + foreach ($hostRes in $port.HostResource) { + if ($hostRes -match 'Msvm_VirtualEthernetSwitch\.CreationClassName="Msvm_VirtualEthernetSwitch",Name="(.+?)"') { + $switchGuid = $Matches[1] + if ($switchLookup.ContainsKey($switchGuid)) { + $switchName = $switchLookup[$switchGuid] + } + } + } + + if ($parentId -and $switchName) { + $adapterSwitchLookup[$parentId] = $switchName + } + } + } + + if ($ManagementOS -or $All) { + # Management OS adapters are associated with the host computer system + $adapters = Get-CimInstance @cimParams -ClassName 'Msvm_InternalEthernetPort' + foreach ($adapter in $adapters) { + # Resolve switch name for management OS adapters via port allocations + $resolvedSwitch = $null + foreach ($port in $portAllocations) { + if ($port.Parent -and $port.Parent -match $adapter.DeviceID) { + foreach ($hostRes in $port.HostResource) { + if ($hostRes -match 'Name="(.+?)"') { + $switchGuid = $Matches[1] + if ($switchLookup.ContainsKey($switchGuid)) { + $resolvedSwitch = $switchLookup[$switchGuid] + } + } + } + break + } + } + + $adapterObject = [PSCustomObject]@{ + Name = $adapter.Name + MacAddress = $adapter.PermanentAddress + SwitchName = $resolvedSwitch + VMName = $null + IsManagement = $true + DeviceId = $adapter.DeviceID + Status = $adapter.StatusDescriptions + } + + [void]$results.Add($adapterObject) + } + } + + if (-not $ManagementOS -or $All) { + # Build the WQL filter for synthetic network adapters (VM NICs) + $filter = $null + if ($VMName) { + # Get the VM's CIM object to find its associated adapters + $vmFilter = "ElementName = '$VMName' AND Caption = 'Virtual Machine'" + $vmCim = Get-CimInstance @cimParams -ClassName 'Msvm_ComputerSystem' -Filter $vmFilter + if ($null -eq $vmCim) { + "Unable to locate virtual machine with name '$VMName'" | Trace-Output -Level:Warning + return + } + } + + # Get all synthetic Ethernet ports (VM network adapters) + $adapters = Get-CimInstance @cimParams -ClassName 'Msvm_SyntheticEthernetPortSettingData' + + # If filtering by VM, get the settings path for that VM + if ($VMName -and $vmCim) { + $vmSettingData = Get-CimAssociatedInstance -InputObject $vmCim -ResultClassName 'Msvm_VirtualSystemSettingData' @cimParams | + Where-Object { $_.VirtualSystemType -eq 'Microsoft:Hyper-V:System:Realized' } + if ($vmSettingData) { + $vmPath = $vmSettingData.CimSystemProperties.CimInstance + $adapters = Get-CimAssociatedInstance -InputObject $vmSettingData -ResultClassName 'Msvm_SyntheticEthernetPortSettingData' @cimParams + } + } + + # Also get emulated adapters (legacy network adapters) if no VM filter is specified + $emulatedAdapters = @() + if (-not $VMName) { + $emulatedAdapters = Get-CimInstance @cimParams -ClassName 'Msvm_EmulatedEthernetPortSettingData' + } + + # Build a lookup of VM names by their settings path + $vmLookup = @{} + $allVmSystems = Get-CimInstance @cimParams -ClassName 'Msvm_ComputerSystem' -Filter "Caption = 'Virtual Machine'" + foreach ($vm in $allVmSystems) { + $vmSettings = Get-CimAssociatedInstance -InputObject $vm -ResultClassName 'Msvm_VirtualSystemSettingData' @cimParams | + Where-Object { $_.VirtualSystemType -eq 'Microsoft:Hyper-V:System:Realized' } + if ($vmSettings) { + $vmLookup[$vmSettings.InstanceID] = $vm.ElementName + } + } + + # Process synthetic adapters + foreach ($adapter in $adapters) { + $vmNameResolved = $null + $parentPath = $adapter.InstanceID + if ($parentPath) { + # InstanceID format: Microsoft:\ - extract the VM GUID portion + $vmGuid = ($parentPath -split '\\')[0] + foreach ($key in $vmLookup.Keys) { + if ($key -like "*$vmGuid*") { + $vmNameResolved = $vmLookup[$key] + break + } + } + } + + $adapterMac = $adapter.Address + if ($MacAddress) { + $formattedMac = Format-SdnMacAddress -MacAddress $MacAddress + $adapterMacFormatted = if ($adapterMac) { Format-SdnMacAddress -MacAddress $adapterMac } else { $null } + if ($adapterMacFormatted -ne $formattedMac) { + continue + } + } + + # Resolve switch name from port allocation lookup + $resolvedSwitch = $adapterSwitchLookup[$adapter.InstanceID] + + $adapterObject = [PSCustomObject]@{ + Name = $adapter.ElementName + MacAddress = $adapterMac + SwitchName = $resolvedSwitch + VMName = $vmNameResolved + IsManagement = $false + InstanceID = $adapter.InstanceID + VirtualSystemIdentifiers = $adapter.VirtualSystemIdentifiers + StaticMacAddress = $adapter.StaticMacAddress + } + + [void]$results.Add($adapterObject) + } + + # Process emulated (legacy) adapters + foreach ($adapter in $emulatedAdapters) { + $vmNameResolved = $null + $parentPath = $adapter.InstanceID + if ($parentPath) { + $vmGuid = ($parentPath -split '\\')[0] + foreach ($key in $vmLookup.Keys) { + if ($key -like "*$vmGuid*") { + $vmNameResolved = $vmLookup[$key] + break + } + } + } + + $adapterMac = $adapter.Address + if ($MacAddress) { + $formattedMac = Format-SdnMacAddress -MacAddress $MacAddress + $adapterMacFormatted = if ($adapterMac) { Format-SdnMacAddress -MacAddress $adapterMac } else { $null } + if ($adapterMacFormatted -ne $formattedMac) { + continue + } + } + + # Resolve switch name from port allocation lookup + $resolvedSwitch = $adapterSwitchLookup[$adapter.InstanceID] + + $adapterObject = [PSCustomObject]@{ + Name = $adapter.ElementName + MacAddress = $adapterMac + SwitchName = $resolvedSwitch + VMName = $vmNameResolved + IsManagement = $false + InstanceID = $adapter.InstanceID + VirtualSystemIdentifiers = $adapter.VirtualSystemIdentifiers + StaticMacAddress = $adapter.StaticMacAddress + } + + [void]$results.Add($adapterObject) + } + } + + return ($results | Sort-Object -Property Name) + } + catch { + $_ | Trace-Exception + $_ | Write-Error + } +} + +function Get-SdnVMCim { + <# + .SYNOPSIS + Retrieves the virtual machines using CIM/WMI for significantly faster performance compared to Get-VM. + .DESCRIPTION + Uses direct CIM queries against the root/virtualization/v2 namespace to retrieve virtual machine information. + This approach is substantially faster than Get-VM on hosts with large numbers of VMs. + .PARAMETER VMName + Specifies the name of the virtual machine to retrieve. If not specified, all virtual machines are returned. + .PARAMETER ComputerName + Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers. + .PARAMETER Credential + Specifies a user account that has permission to perform this action. The default is the current user. + .EXAMPLE + PS> Get-SdnVMCim + .EXAMPLE + PS> Get-SdnVMCim -VMName 'VM01' + .EXAMPLE + PS> Get-SdnVMCim -ComputerName 'Server01','Server02' -Credential (Get-Credential) + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [string]$VMName, + + [Parameter(Mandatory = $false)] + [System.String[]]$ComputerName, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential = [System.Management.Automation.PSCredential]::Empty + ) + + $cimNamespace = 'root/virtualization/v2' + $cimParams = @{ + Namespace = $cimNamespace + ErrorAction = 'Stop' + } + + if ($ComputerName) { + $sessionParams = @{ + ComputerName = $ComputerName + } + if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { + $sessionParams.Add('Credential', $Credential) + } + + $cimSession = New-SdnCimSession @sessionParams + $cimParams.Add('CimSession', $cimSession) + } + + try { + $filter = "Caption = 'Virtual Machine'" + if ($VMName) { + $filter += " AND ElementName = '$VMName'" + } + + $vmSystems = Get-CimInstance @cimParams -ClassName 'Msvm_ComputerSystem' -Filter $filter + $results = [System.Collections.ArrayList]::new() + + foreach ($vm in $vmSystems) { + # Map EnabledState to friendly status + $state = switch ($vm.EnabledState) { + 2 { 'Running' } + 3 { 'Off' } + 6 { 'Saved' } + 9 { 'Paused' } + 32768 { 'Starting' } + 32769 { 'Saving' } + 32770 { 'Stopping' } + 32771 { 'Pausing' } + 32773 { 'Resuming' } + 32776 { 'FastSaved' } + 32777 { 'FastSaving' } + default { 'Unknown' } + } + + # Get the associated settings to retrieve additional VM details + $vmSettings = Get-CimAssociatedInstance -InputObject $vm -ResultClassName 'Msvm_VirtualSystemSettingData' @cimParams | + Where-Object { $_.VirtualSystemType -eq 'Microsoft:Hyper-V:System:Realized' } + + # Get network adapters associated with this VM + $networkAdapters = @() + if ($vmSettings) { + $networkAdapters = Get-CimAssociatedInstance -InputObject $vmSettings -ResultClassName 'Msvm_SyntheticEthernetPortSettingData' @cimParams + } + + $vmObject = [PSCustomObject]@{ + Name = $vm.ElementName + VMId = $vm.Name + State = $state + EnabledState = $vm.EnabledState + HealthState = $vm.HealthState + OnTimeInMilliseconds = $vm.OnTimeInMilliseconds + InstallDate = $vm.InstallDate + NetworkAdapters = @(foreach ($adapter in $networkAdapters) { + [PSCustomObject]@{ + Name = $adapter.ElementName + MacAddress = $adapter.Address + StaticMacAddress = $adapter.StaticMacAddress + InstanceID = $adapter.InstanceID + VirtualSystemIdentifiers = $adapter.VirtualSystemIdentifiers + } + }) + } + + [void]$results.Add($vmObject) + } + + return ($results | Sort-Object -Property Name) + } + catch { + $_ | Trace-Exception + $_ | Write-Error + } +} + function Get-SdnVMNetworkAdapterPortProfile { <# .SYNOPSIS Retrieves the port profile applied to the virtual machine network interfaces. + .DESCRIPTION + Uses CIM queries against root/virtualization/v2 for faster enumeration of port profiles + compared to Get-VMSwitchExtensionPortFeature on hosts with many VMs. .PARAMETER VMName Specifies the name of the virtual machine to be retrieved. .PARAMETER All Switch to indicate to get all the virtual machines network interfaces on the hypervisor host. .PARAMETER ManagementOS When true, displays Port Profiles of Host VNics. Otherwise displays Port Profiles of Vm VNics. + .PARAMETER ComputerName + Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers. + .PARAMETER Credential + Specifies a user account that has permission to perform this action. The default is the current user. .EXAMPLE Get-SdnVMNetworkAdapterPortProfile -VMName 'VM01' .EXAMPLE @@ -2629,38 +3123,122 @@ function Get-SdnVMNetworkAdapterPortProfile { [Switch]$All, [Parameter(Mandatory = $true, ParameterSetName = 'Management')] - [switch]$ManagementOS + [switch]$ManagementOS, + + [Parameter(Mandatory = $false)] + [System.String[]]$ComputerName, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential = [System.Management.Automation.PSCredential]::Empty ) - [System.Guid]$portProfileFeatureId = "9940cd46-8b06-43bb-b9d5-93d50381fd56" - $array = @() + $cimNamespace = 'root/virtualization/v2' + $cimParams = @{ + Namespace = $cimNamespace + ErrorAction = 'Stop' + } + + if ($ComputerName) { + $sessionParams = @{ + ComputerName = $ComputerName + } + if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { + $sessionParams.Add('Credential', $Credential) + } + + $cimSession = New-SdnCimSession @sessionParams + $cimParams.Add('CimSession', $cimSession) + } try { - $netAdapters = Get-SdnVMNetworkAdapter @PSBoundParameters + $results = [System.Collections.ArrayList]::new() + + # Get all ethernet port allocation setting data (represents each port connected to a switch) + $portAllocations = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetPortAllocationSettingData' + + # Get port security settings which contain the profile data + $securitySettings = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetSwitchPortSecuritySettingData' + + # Build lookup of security settings by port path + $securityLookup = @{} + foreach ($security in $securitySettings) { + $portPath = $security.InstanceID -replace '/security$', '' + $securityLookup[$portPath] = $security + } + + # Get the VM network adapters using CIM, passing through connectivity params + $adapterParams = @{} + if ($VMName) { $adapterParams.Add('VMName', $VMName) } + if ($All) { $adapterParams.Add('All', $true) } + if ($ManagementOS) { $adapterParams.Add('ManagementOS', $true) } + if ($ComputerName) { $adapterParams.Add('ComputerName', $ComputerName) } + if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { + $adapterParams.Add('Credential', $Credential) + } + + $netAdapters = Get-SdnVMNetworkAdapterCim @adapterParams + + if ($Name) { + $netAdapters = $netAdapters | Where-Object { $_.Name -eq $Name } + } + foreach ($adapter in $netAdapters) { $object = [VMNetAdapterPortProfile]@{ - VMName = $adapter.VMName - Name = $adapter.Name - MacAddress = $adapter.MacAddress + VMName = $adapter.VMName + Name = $adapter.Name + MacAddress = $adapter.MacAddress } - $currentProfile = Get-VMSwitchExtensionPortFeature -FeatureId $portProfileFeatureId -VMNetworkAdapter $adapter - if ($currentProfile) { - $object.ProfileId = $currentProfile.SettingData.ProfileId - $object.ProfileData = $currentProfile.SettingData.ProfileData + # Find the port allocation for this adapter to get port security settings + $adapterMac = if ($adapter.MacAddress) { Format-SdnMacAddress -MacAddress $adapter.MacAddress } else { $null } + + # Match port allocation by MAC address + $matchedPort = $null + foreach ($port in $portAllocations) { + if ($port.Address -and $adapterMac) { + $portMac = Format-SdnMacAddress -MacAddress $port.Address + if ($portMac -eq $adapterMac) { + $matchedPort = $port + break + } + } } - # we will typically see multiple port data values for each adapter, however the deviceid should be the same across all of the objects - # defensive coding in place for situation where vm is not in proper state and this portdata is null - $portData = (Get-VMSwitchExtensionPortData -VMNetworkAdapter $adapter) - if ($portData) { - $object.PortName = $portData[0].data.deviceid + if ($matchedPort) { + # Look up security settings for this port + $portInstancePath = $matchedPort.InstanceID + $security = $securityLookup[$portInstancePath] + if ($null -eq $security) { + # Try matching by partial path + foreach ($key in $securityLookup.Keys) { + if ($portInstancePath -and $key -like "*$($matchedPort.InstanceID)*") { + $security = $securityLookup[$key] + break + } + } + } + + if ($security) { + if ($security.PortProfileId) { + $object.ProfileId = $security.PortProfileId + } + if ($null -ne $security.PortProfileData) { + $object.ProfileData = $security.PortProfileData + } + } + + # Get the port name from the allocation + if ($matchedPort.InstanceID) { + $object.PortName = $matchedPort.InstanceID + } } - $array += $object + [void]$results.Add($object) } - return ($array | Sort-Object -Property Name) + return ($results | Sort-Object -Property Name) } catch { $_ | Trace-Exception @@ -2769,6 +3347,9 @@ function Set-SdnVMNetworkAdapterPortProfile { <# .SYNOPSIS Configures the port profile applied to the virtual machine network interfaces. + .DESCRIPTION + Uses CIM methods against root/virtualization/v2 to set port profile settings + for faster performance compared to Get-VMSwitchExtensionPortFeature/Set-VMSwitchExtensionPortFeature. .PARAMETER VMName Specifies the name of the virtual machine. .PARAMETER MacAddress @@ -2839,50 +3420,99 @@ function Set-SdnVMNetworkAdapterPortProfile { [switch]$HostVmNic ) - if ($null -eq (Get-Module -Name Hyper-V)) { - Import-Module -Name Hyper-V -Force -ErrorAction Stop + $cimNamespace = 'root/virtualization/v2' + $cimParams = @{ + Namespace = $cimNamespace + ErrorAction = 'Stop' } - [System.Guid]$portProfileFeatureId = "9940cd46-8b06-43bb-b9d5-93d50381fd56" - [System.Guid]$vendorId = "1FA41B39-B444-4E43-B35A-E1F7985FD548" - $vmAdapterParams = @{ - VMName = $VMName - MacAddress = $MacAddress - } + [System.Guid]$vendorId = "1FA41B39-B444-4E43-B35A-E1F7985FD548" + $formattedMac = Format-SdnMacAddress -MacAddress $MacAddress + + # Locate the VM network adapter via CIM + $adapterParams = @{ VMName = $VMName; MacAddress = $MacAddress } if ($HostVmNic) { - $vmAdapterParams.Add('ManagementOS', $true) + $adapterParams = @{ ManagementOS = $true } + } + + $vmNic = Get-SdnVMNetworkAdapterCim @adapterParams + if ($HostVmNic -and $vmNic) { + $vmNic = $vmNic | Where-Object { (Format-SdnMacAddress -MacAddress $_.MacAddress) -eq $formattedMac } } - $vmNic = Get-SdnVmNetworkAdapter @vmAdapterParams if ($null -eq $vmNic) { throw New-Object System.ArgumentException("Unable to locate VM $VMName with MacAddress $MacAddress") } - $portProfileDefaultSetting = Get-VMSystemSwitchExtensionPortFeature -FeatureId $portProfileFeatureId -ErrorAction Stop - $portProfileDefaultSetting.SettingData.ProfileId = $ProfileId.ToString("B") - $portProfileDefaultSetting.SettingData.NetCfgInstanceId = "{56785678-a0e5-4a26-bc9b-c0cba27311a3}" - $portProfileDefaultSetting.SettingData.CdnLabelString = "TestCdn" - $portProfileDefaultSetting.SettingData.CdnLabelId = 1111 - $portProfileDefaultSetting.SettingData.ProfileName = "Testprofile" - $portProfileDefaultSetting.SettingData.VendorId = $vendorId.ToString("B") - $portProfileDefaultSetting.SettingData.VendorName = "NetworkController" - $portProfileDefaultSetting.SettingData.ProfileData = $ProfileData + # Find the port allocation for this adapter + $portAllocations = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetPortAllocationSettingData' + $matchedPort = $null + foreach ($port in $portAllocations) { + if ($port.Address) { + $portMac = Format-SdnMacAddress -MacAddress $port.Address + if ($portMac -eq $formattedMac) { + $matchedPort = $port + break + } + } + } + + if ($null -eq $matchedPort) { + throw New-Object System.ArgumentException("Unable to locate port allocation for adapter with MacAddress $MacAddress") + } - $currentProfile = Get-VMSwitchExtensionPortFeature -FeatureId $portProfileFeatureId -VMNetworkAdapter $vmNic - if ($null -eq $currentProfile) { - Add-VMSwitchExtensionPortFeature -VMSwitchExtensionFeature $portProfileDefaultSetting -VMNetworkAdapter $vmNic + # Find existing security settings for this port + $securitySettings = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetSwitchPortSecuritySettingData' + $existingSecurity = $null + $portInstancePath = $matchedPort.InstanceID + foreach ($security in $securitySettings) { + $securityPortPath = $security.InstanceID -replace '/security$', '' + if ($securityPortPath -eq $portInstancePath) { + $existingSecurity = $security + break + } } - else { - "Current Settings: ProfileId [{0}] ProfileData [{1}]" -f $currentProfile.SettingData.ProfileId, $currentProfile.SettingData.ProfileData | Trace-Output - $currentProfile.SettingData.ProfileId = $ProfileId.ToString("B") - $currentProfile.SettingData.ProfileData = $ProfileData - $currentProfile.SettingData.VendorId = $vendorId.ToString("B") + if ($existingSecurity) { + "Current Settings: ProfileId [{0}] ProfileData [{1}]" -f $existingSecurity.PortProfileId, $existingSecurity.PortProfileData | Trace-Output - Set-VMSwitchExtensionPortFeature -VMSwitchExtensionFeature $currentProfile -VMNetworkAdapter $vmNic + # Update existing security settings via CIM + $existingSecurity.PortProfileId = $ProfileId.ToString("B") + $existingSecurity.PortProfileData = $ProfileData + $existingSecurity.PortProfileVendorId = $vendorId.ToString("B") + Set-CimInstance -InputObject $existingSecurity -ErrorAction Stop + } + else { + # Create new security settings via the virtual switch management service + $vsms = Get-CimInstance @cimParams -ClassName 'Msvm_VirtualEthernetSwitchManagementService' + $securitySettingData = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetSwitchPortSecuritySettingData' | Select-Object -First 0 + + # Use the switch management service to add port feature settings + # Fall back to Hyper-V cmdlet for creating new port profiles as CIM creation requires complex WMI method invocation + if ($null -eq (Get-Module -Name Hyper-V)) { + Import-Module -Name Hyper-V -Force -ErrorAction Stop + } + + [System.Guid]$portProfileFeatureId = "9940cd46-8b06-43bb-b9d5-93d50381fd56" + $portProfileDefaultSetting = Get-VMSystemSwitchExtensionPortFeature -FeatureId $portProfileFeatureId -ErrorAction Stop + $portProfileDefaultSetting.SettingData.ProfileId = $ProfileId.ToString("B") + $portProfileDefaultSetting.SettingData.NetCfgInstanceId = "{56785678-a0e5-4a26-bc9b-c0cba27311a3}" + $portProfileDefaultSetting.SettingData.CdnLabelString = "TestCdn" + $portProfileDefaultSetting.SettingData.CdnLabelId = 1111 + $portProfileDefaultSetting.SettingData.ProfileName = "Testprofile" + $portProfileDefaultSetting.SettingData.VendorId = $vendorId.ToString("B") + $portProfileDefaultSetting.SettingData.VendorName = "NetworkController" + $portProfileDefaultSetting.SettingData.ProfileData = $ProfileData + + # Need the full VMNetworkAdapter object for Add-VMSwitchExtensionPortFeature + $fullVmNic = Get-VMNetworkAdapter -VMName $VMName | Where-Object { (Format-SdnMacAddress -MacAddress $_.MacAddress) -eq $formattedMac } + if ($HostVmNic) { + $fullVmNic = Get-VMNetworkAdapter -ManagementOS | Where-Object { (Format-SdnMacAddress -MacAddress $_.MacAddress) -eq $formattedMac } + } + Add-VMSwitchExtensionPortFeature -VMSwitchExtensionFeature $portProfileDefaultSetting -VMNetworkAdapter $fullVmNic } - "Successfully created/added Port Profile for VM [{0})], Adapter [{1}], PortProfileId [{2}], ProfileData [{3}]" -f $vmNic.VMName, $vmNic.Name, $ProfileId.ToString(), $ProfileData | Trace-Output + "Successfully created/added Port Profile for VM [{0})], MacAddress [{1}], PortProfileId [{2}], ProfileData [{3}]" -f $VMName, $MacAddress, $ProfileId.ToString(), $ProfileData | Trace-Output } $splat = @{ diff --git a/src/modules/SdnDiag.Utilities.psm1 b/src/modules/SdnDiag.Utilities.psm1 index ccd93c17..75835e94 100644 --- a/src/modules/SdnDiag.Utilities.psm1 +++ b/src/modules/SdnDiag.Utilities.psm1 @@ -1829,6 +1829,126 @@ function Invoke-WebRequestWithRetry { return $result } +function New-SdnCimSession { + <# + .SYNOPSIS + Creates or retrieves an existing CIM session for the specified computer(s). + .DESCRIPTION + Manages CIM sessions similarly to how New-PSRemotingSession manages PSSessions. + Sessions are named with an 'SdnDiag-Cim-' prefix and reused when already open, + avoiding repeated connection overhead on hosts with many CIM queries. + .PARAMETER ComputerName + Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers. + .PARAMETER Credential + Specifies a user account that has permission to perform this action. The default is the current user. + .PARAMETER Force + Forces creation of a new session even if one already exists for the target computer. + .EXAMPLE + PS> New-SdnCimSession -ComputerName 'Server01' + .EXAMPLE + PS> New-SdnCimSession -ComputerName 'Server01','Server02' -Credential (Get-Credential) + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.String[]]$ComputerName, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential = [System.Management.Automation.PSCredential]::Empty, + + [Parameter(Mandatory = $false)] + [Switch]$Force + ) + + begin { + $cimSessions = @() + $currentActiveSessions = Get-CimSession | Where-Object { $_.Name -like 'SdnDiag-Cim-*' -and $_.TestConnection() } + } + process { + $ComputerName | ForEach-Object { + $objectName = $PSItem + + # check to see if session is already opened and available + if ($currentActiveSessions.ComputerName -contains $objectName -and !$Force) { + $session = ($currentActiveSessions | Where-Object { $_.ComputerName -eq $objectName })[0] + "Located existing CIM session {0} for {1}" -f $session.Name, $objectName | Trace-Output -Level:Verbose + $cimSessions += $session + return # stop processing this computer + } + + try { + $sessionParams = @{ + ComputerName = $objectName + Name = "SdnDiag-Cim-$(Get-Random)" + ErrorAction = 'Stop' + } + + if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { + "CimSession use user-defined credential" | Trace-Output -Level:Verbose + $sessionParams.Add('Credential', $Credential) + } + + "Creating CIM session to {0}" -f $objectName | Trace-Output -Level:Verbose + $session = New-CimSession @sessionParams + $cimSessions += $session + } + catch { + "Unable to create CIM session to {0}. Error: {1}" -f $objectName, $_.Exception.Message | Trace-Output -Level:Error + $_ | Trace-Exception + } + } + } + end { + return $cimSessions + } +} + +function Remove-SdnCimSession { + <# + .SYNOPSIS + Gracefully removes any existing SdnDiag CIM sessions. + .PARAMETER ComputerName + The computer name(s) that should have any existing CIM sessions removed. + If not specified, all SdnDiag CIM sessions are removed. + .EXAMPLE + PS> Remove-SdnCimSession + .EXAMPLE + PS> Remove-SdnCimSession -ComputerName 'Server01','Server02' + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [System.String[]]$ComputerName + ) + + try { + if ($PSBoundParameters.ContainsKey('ComputerName')) { + $sessions = Get-CimSession | Where-Object { $_.Name -like 'SdnDiag-Cim-*' -and $_.ComputerName -iin $ComputerName } + } + else { + $sessions = Get-CimSession | Where-Object { $_.Name -like 'SdnDiag-Cim-*' } + } + + foreach ($session in $sessions) { + "Removing CIM session {0} for {1}" -f $session.Name, $session.ComputerName | Trace-Output -Level:Verbose + try { + $session | Remove-CimSession -ErrorAction Stop + } + catch { + "Unable to remove CIM session {0} for {1}. Error: {2}" -f $session.Name, $session.ComputerName, $_.Exception.Message | Trace-Output -Level:Warning + } + } + } + catch { + $_ | Trace-Exception + $_ | Write-Error + } +} + function New-PSRemotingSession { [CmdletBinding()] param ( From a41904942c076bc233b2f43b492ef7f289e0604d Mon Sep 17 00:00:00 2001 From: Adam Rudell Date: Thu, 13 Aug 2026 14:50:59 -0500 Subject: [PATCH 2/6] Add Pester tests for CIM functions and session management - Add Get-SdnCimAssociatedInstance wrapper for testability (Get-CimAssociatedInstance requires [CimInstance] typed InputObject which prevents Pester mock interception) - Add 12 tests for Get-SdnVMSwitchCim, Get-SdnVMNetworkAdapterCim, Get-SdnVMCim, and Get-SdnVMNetworkAdapterPortProfile in Server.Tests.ps1 - Add 7 tests for New-SdnCimSession and Remove-SdnCimSession in Utilities.Tests.ps1 - Fix Remove-SdnCimSession to use -Id parameter instead of pipeline for mockability - Use broad mock pattern with switch routing for Get-CimInstance (ParameterFilter does not reliably intercept splatted parameters) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 220ab25a-0e38-4217-8e3c-e0a2e22fd7d9 --- src/modules/SdnDiag.Server.psm1 | 44 +++- src/modules/SdnDiag.Utilities.psm1 | 2 +- tests/offline/Server.Tests.ps1 | 336 +++++++++++++++++++++++++++++ tests/offline/Utilities.Tests.ps1 | 122 +++++++++++ 4 files changed, 498 insertions(+), 6 deletions(-) diff --git a/src/modules/SdnDiag.Server.psm1 b/src/modules/SdnDiag.Server.psm1 index 8a14ff98..78203379 100644 --- a/src/modules/SdnDiag.Server.psm1 +++ b/src/modules/SdnDiag.Server.psm1 @@ -2589,6 +2589,40 @@ function Get-SdnVMNetworkAdapter { } +function Get-SdnCimAssociatedInstance { + <# + .SYNOPSIS + Thin wrapper around Get-CimAssociatedInstance that accepts untyped InputObject for testability. + .DESCRIPTION + Get-CimAssociatedInstance requires a [CimInstance] typed InputObject parameter, which prevents + Pester mocks from intercepting calls when PSCustomObjects are passed during unit tests. This + wrapper accepts any object type, making it mockable while preserving all functionality. + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + $InputObject, + + [Parameter(Mandatory = $false)] + [string]$ResultClassName, + + [Parameter(Mandatory = $false)] + [string]$Namespace, + + [Parameter(Mandatory = $false)] + [string]$ErrorAction + ) + + $params = @{ + InputObject = $InputObject + } + if ($ResultClassName) { $params['ResultClassName'] = $ResultClassName } + if ($Namespace) { $params['Namespace'] = $Namespace } + + return (Get-CimAssociatedInstance @params) +} + function Get-SdnVMSwitchCim { <# .SYNOPSIS @@ -2853,11 +2887,11 @@ function Get-SdnVMNetworkAdapterCim { # If filtering by VM, get the settings path for that VM if ($VMName -and $vmCim) { - $vmSettingData = Get-CimAssociatedInstance -InputObject $vmCim -ResultClassName 'Msvm_VirtualSystemSettingData' @cimParams | + $vmSettingData = Get-SdnCimAssociatedInstance -InputObject $vmCim -ResultClassName 'Msvm_VirtualSystemSettingData' @cimParams | Where-Object { $_.VirtualSystemType -eq 'Microsoft:Hyper-V:System:Realized' } if ($vmSettingData) { $vmPath = $vmSettingData.CimSystemProperties.CimInstance - $adapters = Get-CimAssociatedInstance -InputObject $vmSettingData -ResultClassName 'Msvm_SyntheticEthernetPortSettingData' @cimParams + $adapters = Get-SdnCimAssociatedInstance -InputObject $vmSettingData -ResultClassName 'Msvm_SyntheticEthernetPortSettingData' @cimParams } } @@ -2871,7 +2905,7 @@ function Get-SdnVMNetworkAdapterCim { $vmLookup = @{} $allVmSystems = Get-CimInstance @cimParams -ClassName 'Msvm_ComputerSystem' -Filter "Caption = 'Virtual Machine'" foreach ($vm in $allVmSystems) { - $vmSettings = Get-CimAssociatedInstance -InputObject $vm -ResultClassName 'Msvm_VirtualSystemSettingData' @cimParams | + $vmSettings = Get-SdnCimAssociatedInstance -InputObject $vm -ResultClassName 'Msvm_VirtualSystemSettingData' @cimParams | Where-Object { $_.VirtualSystemType -eq 'Microsoft:Hyper-V:System:Realized' } if ($vmSettings) { $vmLookup[$vmSettings.InstanceID] = $vm.ElementName @@ -3048,13 +3082,13 @@ function Get-SdnVMCim { } # Get the associated settings to retrieve additional VM details - $vmSettings = Get-CimAssociatedInstance -InputObject $vm -ResultClassName 'Msvm_VirtualSystemSettingData' @cimParams | + $vmSettings = Get-SdnCimAssociatedInstance -InputObject $vm -ResultClassName 'Msvm_VirtualSystemSettingData' @cimParams | Where-Object { $_.VirtualSystemType -eq 'Microsoft:Hyper-V:System:Realized' } # Get network adapters associated with this VM $networkAdapters = @() if ($vmSettings) { - $networkAdapters = Get-CimAssociatedInstance -InputObject $vmSettings -ResultClassName 'Msvm_SyntheticEthernetPortSettingData' @cimParams + $networkAdapters = Get-SdnCimAssociatedInstance -InputObject $vmSettings -ResultClassName 'Msvm_SyntheticEthernetPortSettingData' @cimParams } $vmObject = [PSCustomObject]@{ diff --git a/src/modules/SdnDiag.Utilities.psm1 b/src/modules/SdnDiag.Utilities.psm1 index 75835e94..528022da 100644 --- a/src/modules/SdnDiag.Utilities.psm1 +++ b/src/modules/SdnDiag.Utilities.psm1 @@ -1936,7 +1936,7 @@ function Remove-SdnCimSession { foreach ($session in $sessions) { "Removing CIM session {0} for {1}" -f $session.Name, $session.ComputerName | Trace-Output -Level:Verbose try { - $session | Remove-CimSession -ErrorAction Stop + Remove-CimSession -Id $session.Id -ErrorAction Stop } catch { "Unable to remove CIM session {0} for {1}. Error: {2}" -f $session.Name, $session.ComputerName, $_.Exception.Message | Trace-Output -Level:Warning diff --git a/tests/offline/Server.Tests.ps1 b/tests/offline/Server.Tests.ps1 index 35030c0f..a1d86612 100644 --- a/tests/offline/Server.Tests.ps1 +++ b/tests/offline/Server.Tests.ps1 @@ -427,3 +427,339 @@ namespace SdnDiagnostics.PesterOffline { } } } + +Describe 'Server - Get-SdnVMSwitchCim' { + + It "Returns virtual switches with friendly names" { + InModuleScope SdnDiag.Server { + Mock Get-CimInstance { + return @( + [PSCustomObject]@{ + ElementName = 'ConvergedSwitch' + Name = 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE' + IOVPreferred = $false + Notes = '' + MaxIOVOffloads = 0 + InstallDate = $null + } + ) + } + Mock New-SdnCimSession { } + + $result = Get-SdnVMSwitchCim + $result | Should -Not -BeNullOrEmpty + $result[0].Name | Should -Be 'ConvergedSwitch' + $result[0].SwitchId | Should -Be 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE' + } + } + + It "Filters by switch name" { + InModuleScope SdnDiag.Server { + Mock Get-CimInstance { + return @( + [PSCustomObject]@{ + ElementName = 'ConvergedSwitch' + Name = 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE' + IOVPreferred = $false + Notes = '' + MaxIOVOffloads = 0 + InstallDate = $null + } + ) + } + Mock New-SdnCimSession { } + + $result = Get-SdnVMSwitchCim -Name 'ConvergedSwitch' + $result | Should -Not -BeNullOrEmpty + $result[0].Name | Should -Be 'ConvergedSwitch' + } + } + + It "Returns empty when no switches exist" { + InModuleScope SdnDiag.Server { + Mock Get-CimInstance { return @() } + Mock New-SdnCimSession { } + + $result = Get-SdnVMSwitchCim + $result | Should -BeNullOrEmpty + } + } +} + +Describe 'Server - Get-SdnVMNetworkAdapterCim' { + + BeforeAll { + # shared mock data for adapter tests + $Global:PesterOfflineTests.CimMockData = @{ + SyntheticAdapter = [PSCustomObject]@{ + ElementName = 'Network Adapter' + Address = '001DD8070001' + InstanceID = 'Microsoft:AAAAAAAA-1111-2222-3333-444444444444\BBBBBBBB-5555-6666-7777-888888888888' + VirtualSystemIdentifiers = @('{BBBBBBBB-5555-6666-7777-888888888888}') + StaticMacAddress = $true + } + VmSystem = [PSCustomObject]@{ + ElementName = 'DVLAB-VM01' + Name = 'AAAAAAAA-1111-2222-3333-444444444444' + EnabledState = 2 + Caption = 'Virtual Machine' + } + VmSettingData = [PSCustomObject]@{ + InstanceID = 'Microsoft:AAAAAAAA-1111-2222-3333-444444444444' + VirtualSystemType = 'Microsoft:Hyper-V:System:Realized' + CimSystemProperties = [PSCustomObject]@{ CimInstance = 'path' } + } + InternalPort = [PSCustomObject]@{ + Name = 'vEthernet (ConvergedSwitch)' + PermanentAddress = '001DD8070099' + DeviceID = '{CCCCCCCC-9999-AAAA-BBBB-CCCCCCCCCCCC}' + StatusDescriptions = @('OK') + } + PortAllocation = [PSCustomObject]@{ + Address = '001DD8070001' + Parent = 'Msvm_SyntheticEthernetPortSettingData.InstanceID="Microsoft:AAAAAAAA-1111-2222-3333-444444444444\\BBBBBBBB-5555-6666-7777-888888888888"' + HostResource = @('Msvm_VirtualEthernetSwitch.CreationClassName="Msvm_VirtualEthernetSwitch",Name="AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE"') + InstanceID = 'Microsoft:AAAAAAAA-1111-2222-3333-444444444444\BBBBBBBB-5555-6666-7777-888888888888\port001' + } + } + } + + It "Returns VM network adapters with VMName and SwitchName resolved" { + InModuleScope SdnDiag.Server { + $mockData = $Global:PesterOfflineTests.CimMockData + + Mock Get-SdnVMSwitchCim { + return @([PSCustomObject]@{ Name = 'ConvergedSwitch'; SwitchId = 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE' }) + } + Mock Get-CimInstance { + switch ($ClassName) { + 'Msvm_EthernetPortAllocationSettingData' { return @($Global:PesterOfflineTests.CimMockData.PortAllocation) } + 'Msvm_SyntheticEthernetPortSettingData' { return @($Global:PesterOfflineTests.CimMockData.SyntheticAdapter) } + 'Msvm_EmulatedEthernetPortSettingData' { return @() } + 'Msvm_ComputerSystem' { return @($Global:PesterOfflineTests.CimMockData.VmSystem) } + default { return @() } + } + } + Mock Get-SdnCimAssociatedInstance { + return $Global:PesterOfflineTests.CimMockData.VmSettingData + } + Mock New-SdnCimSession { } + + $result = Get-SdnVMNetworkAdapterCim + $result | Should -Not -BeNullOrEmpty + $result[0].MacAddress | Should -Be '001DD8070001' + $result[0].VMName | Should -Be 'DVLAB-VM01' + $result[0].SwitchName | Should -Be 'ConvergedSwitch' + $result[0].IsManagement | Should -BeFalse + } + } + + It "Returns Management OS adapters when -ManagementOS is specified" { + InModuleScope SdnDiag.Server { + Mock Get-SdnVMSwitchCim { + return @([PSCustomObject]@{ Name = 'ConvergedSwitch'; SwitchId = 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE' }) + } + Mock Get-CimInstance { + switch ($ClassName) { + 'Msvm_EthernetPortAllocationSettingData' { return @() } + 'Msvm_InternalEthernetPort' { + return @($Global:PesterOfflineTests.CimMockData.InternalPort) + } + default { return @() } + } + } + Mock New-SdnCimSession { } + + $result = Get-SdnVMNetworkAdapterCim -ManagementOS + $result | Should -Not -BeNullOrEmpty + $result[0].IsManagement | Should -BeTrue + $result[0].Name | Should -Be 'vEthernet (ConvergedSwitch)' + } + } + + It "Returns both VM and Management OS adapters when -All is specified" { + InModuleScope SdnDiag.Server { + $mockData = $Global:PesterOfflineTests.CimMockData + + Mock Get-SdnVMSwitchCim { + return @([PSCustomObject]@{ Name = 'ConvergedSwitch'; SwitchId = 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE' }) + } + Mock Get-CimInstance { + switch ($ClassName) { + 'Msvm_EthernetPortAllocationSettingData' { return @($Global:PesterOfflineTests.CimMockData.PortAllocation) } + 'Msvm_InternalEthernetPort' { return @($Global:PesterOfflineTests.CimMockData.InternalPort) } + 'Msvm_SyntheticEthernetPortSettingData' { return @($Global:PesterOfflineTests.CimMockData.SyntheticAdapter) } + 'Msvm_EmulatedEthernetPortSettingData' { return @() } + 'Msvm_ComputerSystem' { return @($Global:PesterOfflineTests.CimMockData.VmSystem) } + default { return @() } + } + } + Mock Get-SdnCimAssociatedInstance { + return $Global:PesterOfflineTests.CimMockData.VmSettingData + } + Mock New-SdnCimSession { } + + $result = Get-SdnVMNetworkAdapterCim -All + $result | Should -Not -BeNullOrEmpty + ($result | Where-Object { $_.IsManagement -eq $true }) | Should -Not -BeNullOrEmpty + ($result | Where-Object { $_.IsManagement -eq $false }) | Should -Not -BeNullOrEmpty + } + } + + It "Filters by VMName" { + InModuleScope SdnDiag.Server { + Mock Get-SdnVMSwitchCim { return @() } + Mock Get-CimInstance { + switch ($ClassName) { + 'Msvm_EthernetPortAllocationSettingData' { return @() } + 'Msvm_ComputerSystem' { return @($Global:PesterOfflineTests.CimMockData.VmSystem) } + 'Msvm_EmulatedEthernetPortSettingData' { return @() } + default { return @() } + } + } + Mock Get-SdnCimAssociatedInstance { + switch ($ResultClassName) { + 'Msvm_VirtualSystemSettingData' { return $Global:PesterOfflineTests.CimMockData.VmSettingData } + 'Msvm_SyntheticEthernetPortSettingData' { return @($Global:PesterOfflineTests.CimMockData.SyntheticAdapter) } + default { return $null } + } + } + Mock New-SdnCimSession { } + + $result = Get-SdnVMNetworkAdapterCim -VMName 'DVLAB-VM01' + $result | Should -Not -BeNullOrEmpty + $result[0].VMName | Should -Be 'DVLAB-VM01' + } + } +} + +Describe 'Server - Get-SdnVMCim' { + + It "Returns VMs with state mapped to friendly names" { + InModuleScope SdnDiag.Server { + Mock Get-CimInstance { + return @( + [PSCustomObject]@{ ElementName = 'DVLAB-VM01'; Name = 'AAAAAAAA-1111-2222-3333-444444444444'; EnabledState = 2; Caption = 'Virtual Machine' }, + [PSCustomObject]@{ ElementName = 'DVLAB-VM02'; Name = 'BBBBBBBB-1111-2222-3333-444444444444'; EnabledState = 3; Caption = 'Virtual Machine' } + ) + } + Mock Get-SdnCimAssociatedInstance { + switch ($ResultClassName) { + 'Msvm_VirtualSystemSettingData' { + return [PSCustomObject]@{ + InstanceID = "Microsoft:$($InputObject.Name)" + VirtualSystemType = 'Microsoft:Hyper-V:System:Realized' + } + } + 'Msvm_SyntheticEthernetPortSettingData' { return @() } + default { return $null } + } + } + Mock New-SdnCimSession { } + + $result = Get-SdnVMCim + $result | Should -HaveCount 2 + $result[0].State | Should -Be 'Running' + $result[1].State | Should -Be 'Off' + } + } + + It "Filters by VMName" { + InModuleScope SdnDiag.Server { + Mock Get-CimInstance { + return @( + [PSCustomObject]@{ ElementName = 'DVLAB-VM01'; Name = 'AAAAAAAA-1111-2222-3333-444444444444'; EnabledState = 2; Caption = 'Virtual Machine' } + ) + } + Mock Get-SdnCimAssociatedInstance { + switch ($ResultClassName) { + 'Msvm_VirtualSystemSettingData' { + return [PSCustomObject]@{ + InstanceID = 'Microsoft:AAAAAAAA-1111-2222-3333-444444444444' + VirtualSystemType = 'Microsoft:Hyper-V:System:Realized' + } + } + 'Msvm_SyntheticEthernetPortSettingData' { return @() } + default { return $null } + } + } + Mock New-SdnCimSession { } + + $result = Get-SdnVMCim -VMName 'DVLAB-VM01' + $result | Should -HaveCount 1 + $result[0].Name | Should -Be 'DVLAB-VM01' + } + } + + It "Returns empty when no VMs exist" { + InModuleScope SdnDiag.Server { + Mock Get-CimInstance { return @() } + Mock Get-SdnCimAssociatedInstance { return $null } + Mock New-SdnCimSession { } + + $result = Get-SdnVMCim + $result | Should -BeNullOrEmpty + } + } +} + +Describe 'Server - Get-SdnVMNetworkAdapterPortProfile (CIM)' { + + It "Returns port profiles with ProfileId and ProfileData" { + InModuleScope SdnDiag.Server { + Mock Get-CimInstance { + switch ($ClassName) { + 'Msvm_EthernetPortAllocationSettingData' { + return @( + [PSCustomObject]@{ + Address = '001DD8070001' + InstanceID = 'Microsoft:AAAAAAAA-1111-2222-3333-444444444444\BBBBBBBB-5555-6666-7777-888888888888\port001' + } + ) + } + 'Msvm_EthernetSwitchPortSecuritySettingData' { + return @( + [PSCustomObject]@{ + InstanceID = 'Microsoft:AAAAAAAA-1111-2222-3333-444444444444\BBBBBBBB-5555-6666-7777-888888888888\port001/security' + PortProfileId = '{11111111-2222-3333-4444-555555555555}' + PortProfileData = 1 + PortProfileVendorId = '{1FA41B39-B444-4E43-B35A-E1F7985FD548}' + } + ) + } + default { return @() } + } + } + Mock Get-SdnVMNetworkAdapterCim { + return @( + [PSCustomObject]@{ + Name = 'Network Adapter' + MacAddress = '001DD8070001' + VMName = 'DVLAB-VM01' + IsManagement = $false + InstanceID = 'Microsoft:AAAAAAAA-1111-2222-3333-444444444444\BBBBBBBB-5555-6666-7777-888888888888' + } + ) + } + Mock New-SdnCimSession { } + + $result = Get-SdnVMNetworkAdapterPortProfile -VMName 'DVLAB-VM01' + $result | Should -Not -BeNullOrEmpty + $result[0].ProfileId | Should -Be '{11111111-2222-3333-4444-555555555555}' + $result[0].ProfileData | Should -Be 1 + $result[0].MacAddress | Should -Be '001DD8070001' + } + } + + It "Passes -All through to Get-SdnVMNetworkAdapterCim" { + InModuleScope SdnDiag.Server { + Mock Get-CimInstance { return @() } + Mock Get-SdnVMNetworkAdapterCim { return @() } + Mock New-SdnCimSession { } + + Get-SdnVMNetworkAdapterPortProfile -All + + Should -Invoke -CommandName Get-SdnVMNetworkAdapterCim -ParameterFilter { $All -eq $true } + } + } +} diff --git a/tests/offline/Utilities.Tests.ps1 b/tests/offline/Utilities.Tests.ps1 index a00aef64..bfd35bf4 100644 --- a/tests/offline/Utilities.Tests.ps1 +++ b/tests/offline/Utilities.Tests.ps1 @@ -177,3 +177,125 @@ Describe 'Utilities - IP Address Validation' { } } } + +Describe 'Utilities - CIM Session Management' { + + Context 'New-SdnCimSession' { + It "Creates a new CIM session when none exists" { + InModuleScope SdnDiag.Utilities { + $mockSession = [PSCustomObject]@{ + Name = 'SdnDiag-Cim-12345' + ComputerName = 'DVLAB-S1-N01' + } + Mock Get-CimSession { return @() } + Mock New-CimSession { return $mockSession } + + $result = New-SdnCimSession -ComputerName 'DVLAB-S1-N01' + $result | Should -Not -BeNullOrEmpty + $result.ComputerName | Should -Be 'DVLAB-S1-N01' + + Should -Invoke -CommandName New-CimSession -Exactly -Times 1 + } + } + + It "Reuses an existing SdnDiag CIM session" { + InModuleScope SdnDiag.Utilities { + $existingSession = [PSCustomObject]@{ + Name = 'SdnDiag-Cim-99999' + ComputerName = 'DVLAB-S1-N01' + } + $existingSession | Add-Member -MemberType ScriptMethod -Name TestConnection -Value { return $true } + Mock Get-CimSession { return @($existingSession) } + Mock New-CimSession { } + + $result = New-SdnCimSession -ComputerName 'DVLAB-S1-N01' + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'SdnDiag-Cim-99999' + + Should -Invoke -CommandName New-CimSession -Exactly -Times 0 + } + } + + It "Creates a new session when -Force is specified" { + InModuleScope SdnDiag.Utilities { + $existingSession = [PSCustomObject]@{ + Name = 'SdnDiag-Cim-99999' + ComputerName = 'DVLAB-S1-N01' + } + $existingSession | Add-Member -MemberType ScriptMethod -Name TestConnection -Value { return $true } + + $newSession = [PSCustomObject]@{ + Name = 'SdnDiag-Cim-11111' + ComputerName = 'DVLAB-S1-N01' + } + Mock Get-CimSession { return @($existingSession) } + Mock New-CimSession { return $newSession } + + $result = New-SdnCimSession -ComputerName 'DVLAB-S1-N01' -Force + $result | Should -Not -BeNullOrEmpty + + Should -Invoke -CommandName New-CimSession -Exactly -Times 1 + } + } + + It "Handles multiple computer names" { + InModuleScope SdnDiag.Utilities { + Mock Get-CimSession { return @() } + Mock New-CimSession -MockWith { + return [PSCustomObject]@{ + Name = "SdnDiag-Cim-$(Get-Random)" + ComputerName = $ComputerName + } + } + + $result = New-SdnCimSession -ComputerName @('DVLAB-S1-N01', 'DVLAB-S1-N02') + $result | Should -HaveCount 2 + + Should -Invoke -CommandName New-CimSession -Exactly -Times 2 + } + } + } + + Context 'Remove-SdnCimSession' { + It "Removes all SdnDiag CIM sessions when no ComputerName is specified" { + InModuleScope SdnDiag.Utilities { + $sessions = @( + [PSCustomObject]@{ Id = 111; Name = 'SdnDiag-Cim-111'; ComputerName = 'DVLAB-S1-N01' }, + [PSCustomObject]@{ Id = 222; Name = 'SdnDiag-Cim-222'; ComputerName = 'DVLAB-S1-N02' } + ) + Mock Get-CimSession { return $sessions } + Mock Remove-CimSession { } + + Remove-SdnCimSession + + Should -Invoke -CommandName Remove-CimSession -Exactly -Times 2 + } + } + + It "Removes only sessions for specified ComputerName" { + InModuleScope SdnDiag.Utilities { + $sessions = @( + [PSCustomObject]@{ Id = 111; Name = 'SdnDiag-Cim-111'; ComputerName = 'DVLAB-S1-N01' }, + [PSCustomObject]@{ Id = 222; Name = 'SdnDiag-Cim-222'; ComputerName = 'DVLAB-S1-N02' } + ) + Mock Get-CimSession { return $sessions } + Mock Remove-CimSession { } + + Remove-SdnCimSession -ComputerName 'DVLAB-S1-N01' + + Should -Invoke -CommandName Remove-CimSession -Exactly -Times 1 + } + } + + It "Does nothing when no SdnDiag sessions exist" { + InModuleScope SdnDiag.Utilities { + Mock Get-CimSession { return @() } + Mock Remove-CimSession { } + + Remove-SdnCimSession + + Should -Invoke -CommandName Remove-CimSession -Exactly -Times 0 + } + } + } +} From 0fab005aa8c62637fd541985966c1822761ecb0b Mon Sep 17 00:00:00 2001 From: Adam Rudell Date: Thu, 13 Aug 2026 16:05:13 -0500 Subject: [PATCH 3/6] fix: address PR review feedback for CIM framework - Fix port profile class: SecuritySettingData -> ProfileSettingData - Fix VM state mapping (32768=Paused, 32769=Saved, etc.) - Restore ConfigState diagnostic exports (ACL, VLAN, Isolation, etc.) - Fix N+1 round-trips: bulk-query VirtualSystemSettingData for vmLookup - Add MacAddress filter on ManagementOS path - Include emulated adapters when filtering by VMName - Add CimSession forwarding to Get-SdnCimAssociatedInstance wrapper - Accept CimSession array in Get-SdnVMSwitchCim - Add Get-SdnVMCim and Get-SdnVMNetworkAdapterCim to manifest exports - Add Write-Error to New-SdnCimSession catch block - Update tests for corrected CIM class and bulk query approach Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 220ab25a-0e38-4217-8e3c-e0a2e22fd7d9 --- src/SdnDiagnostics.psd1 | 3 + src/modules/SdnDiag.Server.psm1 | 152 +++++++++++++++++++---------- src/modules/SdnDiag.Utilities.psm1 | 1 + tests/offline/Server.Tests.ps1 | 19 ++-- 4 files changed, 114 insertions(+), 61 deletions(-) diff --git a/src/SdnDiagnostics.psd1 b/src/SdnDiagnostics.psd1 index 453eccdf..01a31ac2 100644 --- a/src/SdnDiagnostics.psd1 +++ b/src/SdnDiagnostics.psd1 @@ -126,8 +126,11 @@ 'Get-SdnVipConfig', 'Get-SdnVfpVmSwitchPort', 'Get-SdnVMNetworkAdapter', + 'Get-SdnVMNetworkAdapterCim', 'Get-SdnVMNetworkAdapterPortProfile', + 'Get-SdnVMCim', 'Get-SdnVMSwitch', + 'Get-SdnVMSwitchCim', 'Get-SdnVfpPortGroup', 'Get-SdnVfpPortLayer', 'Get-SdnVfpPortRule', diff --git a/src/modules/SdnDiag.Server.psm1 b/src/modules/SdnDiag.Server.psm1 index 78203379..a9c0a8f7 100644 --- a/src/modules/SdnDiag.Server.psm1 +++ b/src/modules/SdnDiag.Server.psm1 @@ -515,6 +515,27 @@ function Get-ServerConfigState { "Failed to enumerate VMNetworkAdapter for {0}" -f $adapter.Name | Trace-Output -Level:Warning } } + + # collect per-VM adapter diagnostic details using Hyper-V cmdlets + try { + $hvAdapters = Get-VMNetworkAdapter -VMName $vm.Name -ErrorAction SilentlyContinue + foreach ($hvAdapter in $hvAdapters) { + $prefix = (Format-SdnMacAddress -MacAddress $hvAdapter.MacAddress) + $hvAdapter.AclList | Remove-PropertiesFromObject -PropertiesToRemove 'ParentAdapter' | + Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_AclList' -FileType txt -Format List + $hvAdapter.ExtendedAclList | Remove-PropertiesFromObject -PropertiesToRemove 'ParentAdapter','CimSession' | + Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_ExtendedAclList' -FileType txt -Format List + $hvAdapter.IsolationSetting | Remove-PropertiesFromObject -PropertiesToRemove 'ParentAdapter','CimSession' | + Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_IsolationSetting' -FileType txt -Format List + $hvAdapter.RoutingDomainList | Remove-PropertiesFromObject -PropertiesToRemove 'ParentAdapter','CimSession' | + Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_RoutingDomainList' -FileType txt -Format List + $hvAdapter.VlanSetting | Remove-PropertiesFromObject -PropertiesToRemove 'ParentAdapter','CimSession' | + Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_VlanSetting' -FileType txt -Format List + } + } + catch { + "Failed to enumerate detailed VMNetworkAdapter settings for VM {0}" -f $vm.Name | Trace-Output -Level:Warning + } } } @@ -525,6 +546,12 @@ function Get-ServerConfigState { # collect the management OS network adapter details # we do not need this information for general vmnetworkadapters as they are already collected above Get-SdnVMNetworkAdapterCim -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapter_ManagementOS' -FileType txt -Format List + + # collect management OS adapter settings that require Hyper-V cmdlets (not available via CIM) + Get-VMNetworkAdapterIsolation -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapterIsolation_ManagementOS' -FileType txt -Format List + Get-VMNetworkAdapterTeamMapping -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapterTeamMapping_ManagementOS' -FileType txt -Format List + Get-VMNetworkAdapterVLAN -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapterVLAN_ManagementOS' -FileType txt -Format List + Get-VMNetworkAdapterRoutingDomainMapping -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapterRoutingDomainMapping_ManagementOS' -FileType txt -Format List } catch { $_ | Trace-Exception @@ -2611,7 +2638,7 @@ function Get-SdnCimAssociatedInstance { [string]$Namespace, [Parameter(Mandatory = $false)] - [string]$ErrorAction + [Microsoft.Management.Infrastructure.CimSession]$CimSession ) $params = @{ @@ -2619,6 +2646,7 @@ function Get-SdnCimAssociatedInstance { } if ($ResultClassName) { $params['ResultClassName'] = $ResultClassName } if ($Namespace) { $params['Namespace'] = $Namespace } + if ($CimSession) { $params['CimSession'] = $CimSession } return (Get-CimAssociatedInstance @params) } @@ -2660,7 +2688,7 @@ function Get-SdnVMSwitchCim { $Credential = [System.Management.Automation.PSCredential]::Empty, [Parameter(Mandatory = $false)] - [Microsoft.Management.Infrastructure.CimSession]$CimSession + [Microsoft.Management.Infrastructure.CimSession[]]$CimSession ) $cimNamespace = 'root/virtualization/v2' @@ -2865,6 +2893,15 @@ function Get-SdnVMNetworkAdapterCim { Status = $adapter.StatusDescriptions } + # Apply MacAddress filter if specified + if ($MacAddress) { + $formattedMac = Format-SdnMacAddress -MacAddress $MacAddress + $adapterMacFormatted = if ($adapter.PermanentAddress) { Format-SdnMacAddress -MacAddress $adapter.PermanentAddress } else { $null } + if ($adapterMacFormatted -ne $formattedMac) { + continue + } + } + [void]$results.Add($adapterObject) } } @@ -2895,20 +2932,34 @@ function Get-SdnVMNetworkAdapterCim { } } - # Also get emulated adapters (legacy network adapters) if no VM filter is specified + # Also get emulated adapters (legacy network adapters) $emulatedAdapters = @() - if (-not $VMName) { + if ($VMName -and $vmSettingData) { + $emulatedAdapters = Get-SdnCimAssociatedInstance -InputObject $vmSettingData -ResultClassName 'Msvm_EmulatedEthernetPortSettingData' @cimParams + } + elseif (-not $VMName) { $emulatedAdapters = Get-CimInstance @cimParams -ClassName 'Msvm_EmulatedEthernetPortSettingData' } - # Build a lookup of VM names by their settings path + # Build a lookup of VM names by their settings InstanceID using bulk queries $vmLookup = @{} $allVmSystems = Get-CimInstance @cimParams -ClassName 'Msvm_ComputerSystem' -Filter "Caption = 'Virtual Machine'" + $allVmSettings = Get-CimInstance @cimParams -ClassName 'Msvm_VirtualSystemSettingData' -Filter "VirtualSystemType = 'Microsoft:Hyper-V:System:Realized'" + + # Build a Name→ElementName map from VMs + $vmNameByGuid = @{} foreach ($vm in $allVmSystems) { - $vmSettings = Get-SdnCimAssociatedInstance -InputObject $vm -ResultClassName 'Msvm_VirtualSystemSettingData' @cimParams | - Where-Object { $_.VirtualSystemType -eq 'Microsoft:Hyper-V:System:Realized' } - if ($vmSettings) { - $vmLookup[$vmSettings.InstanceID] = $vm.ElementName + $vmNameByGuid[$vm.Name] = $vm.ElementName + } + + # Build InstanceID→VMName lookup from settings (InstanceID contains the VM GUID) + foreach ($setting in $allVmSettings) { + # InstanceID format: Microsoft: + if ($setting.InstanceID -match 'Microsoft:(.+)$') { + $settingVmGuid = $Matches[1] + if ($vmNameByGuid.ContainsKey($settingVmGuid)) { + $vmLookup[$setting.InstanceID] = $vmNameByGuid[$settingVmGuid] + } } } @@ -3071,13 +3122,14 @@ function Get-SdnVMCim { 3 { 'Off' } 6 { 'Saved' } 9 { 'Paused' } - 32768 { 'Starting' } - 32769 { 'Saving' } - 32770 { 'Stopping' } - 32771 { 'Pausing' } - 32773 { 'Resuming' } - 32776 { 'FastSaved' } - 32777 { 'FastSaving' } + 32768 { 'Paused' } + 32769 { 'Saved' } + 32770 { 'Starting' } + 32771 { 'Snapshotting' } + 32773 { 'Saving' } + 32774 { 'Stopping' } + 32776 { 'Pausing' } + 32777 { 'Resuming' } default { 'Unknown' } } @@ -3192,14 +3244,14 @@ function Get-SdnVMNetworkAdapterPortProfile { # Get all ethernet port allocation setting data (represents each port connected to a switch) $portAllocations = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetPortAllocationSettingData' - # Get port security settings which contain the profile data - $securitySettings = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetSwitchPortSecuritySettingData' + # Get port profile settings which contain the profile data + $profileSettings = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetSwitchPortProfileSettingData' - # Build lookup of security settings by port path - $securityLookup = @{} - foreach ($security in $securitySettings) { - $portPath = $security.InstanceID -replace '/security$', '' - $securityLookup[$portPath] = $security + # Build lookup of profile settings by port path + $profileLookup = @{} + foreach ($profile in $profileSettings) { + $portPath = $profile.InstanceID -replace '/[^/]+$', '' + $profileLookup[$portPath] = $profile } # Get the VM network adapters using CIM, passing through connectivity params @@ -3241,25 +3293,25 @@ function Get-SdnVMNetworkAdapterPortProfile { } if ($matchedPort) { - # Look up security settings for this port + # Look up profile settings for this port $portInstancePath = $matchedPort.InstanceID - $security = $securityLookup[$portInstancePath] - if ($null -eq $security) { + $profile = $profileLookup[$portInstancePath] + if ($null -eq $profile) { # Try matching by partial path - foreach ($key in $securityLookup.Keys) { + foreach ($key in $profileLookup.Keys) { if ($portInstancePath -and $key -like "*$($matchedPort.InstanceID)*") { - $security = $securityLookup[$key] + $profile = $profileLookup[$key] break } } } - if ($security) { - if ($security.PortProfileId) { - $object.ProfileId = $security.PortProfileId + if ($profile) { + if ($profile.ProfileId) { + $object.ProfileId = $profile.ProfileId } - if ($null -ne $security.PortProfileData) { - $object.ProfileData = $security.PortProfileData + if ($null -ne $profile.ProfileData) { + $object.ProfileData = $profile.ProfileData } } @@ -3495,33 +3547,29 @@ function Set-SdnVMNetworkAdapterPortProfile { throw New-Object System.ArgumentException("Unable to locate port allocation for adapter with MacAddress $MacAddress") } - # Find existing security settings for this port - $securitySettings = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetSwitchPortSecuritySettingData' - $existingSecurity = $null + # Find existing profile settings for this port + $profileSettings = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetSwitchPortProfileSettingData' + $existingProfile = $null $portInstancePath = $matchedPort.InstanceID - foreach ($security in $securitySettings) { - $securityPortPath = $security.InstanceID -replace '/security$', '' - if ($securityPortPath -eq $portInstancePath) { - $existingSecurity = $security + foreach ($profile in $profileSettings) { + $profilePortPath = $profile.InstanceID -replace '/[^/]+$', '' + if ($profilePortPath -eq $portInstancePath) { + $existingProfile = $profile break } } - if ($existingSecurity) { - "Current Settings: ProfileId [{0}] ProfileData [{1}]" -f $existingSecurity.PortProfileId, $existingSecurity.PortProfileData | Trace-Output + if ($existingProfile) { + "Current Settings: ProfileId [{0}] ProfileData [{1}]" -f $existingProfile.ProfileId, $existingProfile.ProfileData | Trace-Output - # Update existing security settings via CIM - $existingSecurity.PortProfileId = $ProfileId.ToString("B") - $existingSecurity.PortProfileData = $ProfileData - $existingSecurity.PortProfileVendorId = $vendorId.ToString("B") - Set-CimInstance -InputObject $existingSecurity -ErrorAction Stop + # Update existing profile settings via CIM + $existingProfile.ProfileId = $ProfileId.ToString("B") + $existingProfile.ProfileData = $ProfileData + $existingProfile.VendorId = $vendorId.ToString("B") + Set-CimInstance -InputObject $existingProfile -ErrorAction Stop } else { - # Create new security settings via the virtual switch management service - $vsms = Get-CimInstance @cimParams -ClassName 'Msvm_VirtualEthernetSwitchManagementService' - $securitySettingData = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetSwitchPortSecuritySettingData' | Select-Object -First 0 - - # Use the switch management service to add port feature settings + # Create new port profile settings # Fall back to Hyper-V cmdlet for creating new port profiles as CIM creation requires complex WMI method invocation if ($null -eq (Get-Module -Name Hyper-V)) { Import-Module -Name Hyper-V -Force -ErrorAction Stop diff --git a/src/modules/SdnDiag.Utilities.psm1 b/src/modules/SdnDiag.Utilities.psm1 index 528022da..58a16368 100644 --- a/src/modules/SdnDiag.Utilities.psm1 +++ b/src/modules/SdnDiag.Utilities.psm1 @@ -1898,6 +1898,7 @@ function New-SdnCimSession { catch { "Unable to create CIM session to {0}. Error: {1}" -f $objectName, $_.Exception.Message | Trace-Output -Level:Error $_ | Trace-Exception + $_ | Write-Error } } } diff --git a/tests/offline/Server.Tests.ps1 b/tests/offline/Server.Tests.ps1 index a1d86612..ba7689ba 100644 --- a/tests/offline/Server.Tests.ps1 +++ b/tests/offline/Server.Tests.ps1 @@ -537,12 +537,11 @@ Describe 'Server - Get-SdnVMNetworkAdapterCim' { 'Msvm_SyntheticEthernetPortSettingData' { return @($Global:PesterOfflineTests.CimMockData.SyntheticAdapter) } 'Msvm_EmulatedEthernetPortSettingData' { return @() } 'Msvm_ComputerSystem' { return @($Global:PesterOfflineTests.CimMockData.VmSystem) } + 'Msvm_VirtualSystemSettingData' { return @($Global:PesterOfflineTests.CimMockData.VmSettingData) } default { return @() } } } - Mock Get-SdnCimAssociatedInstance { - return $Global:PesterOfflineTests.CimMockData.VmSettingData - } + Mock Get-SdnCimAssociatedInstance { return $null } Mock New-SdnCimSession { } $result = Get-SdnVMNetworkAdapterCim @@ -591,11 +590,12 @@ Describe 'Server - Get-SdnVMNetworkAdapterCim' { 'Msvm_SyntheticEthernetPortSettingData' { return @($Global:PesterOfflineTests.CimMockData.SyntheticAdapter) } 'Msvm_EmulatedEthernetPortSettingData' { return @() } 'Msvm_ComputerSystem' { return @($Global:PesterOfflineTests.CimMockData.VmSystem) } + 'Msvm_VirtualSystemSettingData' { return @($Global:PesterOfflineTests.CimMockData.VmSettingData) } default { return @() } } } Mock Get-SdnCimAssociatedInstance { - return $Global:PesterOfflineTests.CimMockData.VmSettingData + return $null } Mock New-SdnCimSession { } @@ -614,6 +614,7 @@ Describe 'Server - Get-SdnVMNetworkAdapterCim' { 'Msvm_EthernetPortAllocationSettingData' { return @() } 'Msvm_ComputerSystem' { return @($Global:PesterOfflineTests.CimMockData.VmSystem) } 'Msvm_EmulatedEthernetPortSettingData' { return @() } + 'Msvm_VirtualSystemSettingData' { return @($Global:PesterOfflineTests.CimMockData.VmSettingData) } default { return @() } } } @@ -717,13 +718,13 @@ Describe 'Server - Get-SdnVMNetworkAdapterPortProfile (CIM)' { } ) } - 'Msvm_EthernetSwitchPortSecuritySettingData' { + 'Msvm_EthernetSwitchPortProfileSettingData' { return @( [PSCustomObject]@{ - InstanceID = 'Microsoft:AAAAAAAA-1111-2222-3333-444444444444\BBBBBBBB-5555-6666-7777-888888888888\port001/security' - PortProfileId = '{11111111-2222-3333-4444-555555555555}' - PortProfileData = 1 - PortProfileVendorId = '{1FA41B39-B444-4E43-B35A-E1F7985FD548}' + InstanceID = 'Microsoft:AAAAAAAA-1111-2222-3333-444444444444\BBBBBBBB-5555-6666-7777-888888888888\port001/profile' + ProfileId = '{11111111-2222-3333-4444-555555555555}' + ProfileData = 1 + VendorId = '{1FA41B39-B444-4E43-B35A-E1F7985FD548}' } ) } From 3347082d80405653b45c10907d5700a15eeb3efc Mon Sep 17 00:00:00 2001 From: Adam Rudell Date: Thu, 13 Aug 2026 16:18:53 -0500 Subject: [PATCH 4/6] Add CIM port setting functions and fix Repair VLAN tests - Add Get-SdnVMNetworkAdapterVlanCim, IsolationCim, ExtendedAclCim, RoutingDomainCim - Update Get-ServerConfigState and Repair-SdnVMNetworkAdapterPortProfile to use CIM VLAN reads - Add Pester tests for all 4 new port setting functions (79 total, 0 failures) - Fix Repair tests to match CIM-based VLAN read path Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 220ab25a-0e38-4217-8e3c-e0a2e22fd7d9 --- src/SdnDiagnostics.psd1 | 4 + src/modules/SdnDiag.Server.psm1 | 564 +++++++++++++++++++++++++++++++- tests/offline/Server.Tests.ps1 | 240 ++++++++++++-- 3 files changed, 774 insertions(+), 34 deletions(-) diff --git a/src/SdnDiagnostics.psd1 b/src/SdnDiagnostics.psd1 index 01a31ac2..04fe4772 100644 --- a/src/SdnDiagnostics.psd1 +++ b/src/SdnDiagnostics.psd1 @@ -127,7 +127,11 @@ 'Get-SdnVfpVmSwitchPort', 'Get-SdnVMNetworkAdapter', 'Get-SdnVMNetworkAdapterCim', + 'Get-SdnVMNetworkAdapterExtendedAclCim', + 'Get-SdnVMNetworkAdapterIsolationCim', 'Get-SdnVMNetworkAdapterPortProfile', + 'Get-SdnVMNetworkAdapterRoutingDomainCim', + 'Get-SdnVMNetworkAdapterVlanCim', 'Get-SdnVMCim', 'Get-SdnVMSwitch', 'Get-SdnVMSwitchCim', diff --git a/src/modules/SdnDiag.Server.psm1 b/src/modules/SdnDiag.Server.psm1 index a9c0a8f7..73362474 100644 --- a/src/modules/SdnDiag.Server.psm1 +++ b/src/modules/SdnDiag.Server.psm1 @@ -547,11 +547,12 @@ function Get-ServerConfigState { # we do not need this information for general vmnetworkadapters as they are already collected above Get-SdnVMNetworkAdapterCim -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapter_ManagementOS' -FileType txt -Format List - # collect management OS adapter settings that require Hyper-V cmdlets (not available via CIM) - Get-VMNetworkAdapterIsolation -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapterIsolation_ManagementOS' -FileType txt -Format List + # collect management OS adapter port settings using CIM + Get-SdnVMNetworkAdapterIsolationCim -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapterIsolation_ManagementOS' -FileType txt -Format List + Get-SdnVMNetworkAdapterVlanCim -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapterVLAN_ManagementOS' -FileType txt -Format List + Get-SdnVMNetworkAdapterRoutingDomainCim -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapterRoutingDomainMapping_ManagementOS' -FileType txt -Format List + # TeamMapping has no CIM equivalent, use Hyper-V cmdlet Get-VMNetworkAdapterTeamMapping -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapterTeamMapping_ManagementOS' -FileType txt -Format List - Get-VMNetworkAdapterVLAN -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapterVLAN_ManagementOS' -FileType txt -Format List - Get-VMNetworkAdapterRoutingDomainMapping -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapterRoutingDomainMapping_ManagementOS' -FileType txt -Format List } catch { $_ | Trace-Exception @@ -3173,6 +3174,555 @@ function Get-SdnVMCim { } } +function Get-SdnVMNetworkAdapterExtendedAclCim { + <# + .SYNOPSIS + Retrieves extended ACL settings for virtual machine network adapters using CIM. + .DESCRIPTION + Uses direct CIM queries against root/virtualization/v2 to retrieve Msvm_EthernetSwitchPortExtendedAclSettingData, + providing significantly faster performance compared to Get-VMNetworkAdapterExtendedAcl. + .PARAMETER VMName + Specifies the name of the virtual machine whose extended ACL settings are to be retrieved. + .PARAMETER MacAddress + Specifies the MAC address of the network adapter to filter results. + .PARAMETER All + Switch to indicate to get all virtual machine network adapter extended ACL settings. + .PARAMETER ManagementOS + Specifies the management operating system adapters. + .PARAMETER ComputerName + Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers. + .PARAMETER Credential + Specifies a user account that has permission to perform this action. The default is the current user. + .EXAMPLE + PS> Get-SdnVMNetworkAdapterExtendedAclCim -ManagementOS + .EXAMPLE + PS> Get-SdnVMNetworkAdapterExtendedAclCim -VMName 'VM01' + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [string]$VMName, + + [Parameter(Mandatory = $false)] + [string]$MacAddress, + + [Parameter(Mandatory = $false)] + [switch]$All, + + [Parameter(Mandatory = $false)] + [switch]$ManagementOS, + + [Parameter(Mandatory = $false)] + [System.String[]]$ComputerName, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential = [System.Management.Automation.PSCredential]::Empty + ) + + $cimParams = @{ + Namespace = 'root/virtualization/v2' + ErrorAction = 'Stop' + } + + if ($ComputerName) { + $sessionParams = @{ ComputerName = $ComputerName } + if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { + $sessionParams.Add('Credential', $Credential) + } + $cimSession = New-SdnCimSession @sessionParams + $cimParams.Add('CimSession', $cimSession) + } + + try { + $results = [System.Collections.ArrayList]::new() + + # Bulk-query port allocations and extended ACL settings + $portAllocations = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetPortAllocationSettingData' + $aclSettings = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetSwitchPortExtendedAclSettingData' + + # Build lookup: port InstanceID -> list of ACL settings + $aclByPort = @{} + foreach ($acl in $aclSettings) { + $portPath = $acl.InstanceID -replace '/[^/]+$', '' + if (-not $aclByPort.ContainsKey($portPath)) { + $aclByPort[$portPath] = [System.Collections.ArrayList]::new() + } + [void]$aclByPort[$portPath].Add($acl) + } + + # Build MAC -> port InstanceID lookup from port allocations + $macToPort = @{} + foreach ($port in $portAllocations) { + if ($port.Address) { + $portMac = Format-SdnMacAddress -MacAddress $port.Address + $macToPort[$portMac] = $port.InstanceID + } + } + + # Get adapters using CIM + $adapterParams = @{} + if ($VMName) { $adapterParams.Add('VMName', $VMName) } + if ($All) { $adapterParams.Add('All', $true) } + if ($ManagementOS) { $adapterParams.Add('ManagementOS', $true) } + if ($ComputerName) { $adapterParams.Add('ComputerName', $ComputerName) } + if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { + $adapterParams.Add('Credential', $Credential) + } + + $netAdapters = Get-SdnVMNetworkAdapterCim @adapterParams + if ($MacAddress) { + $formattedFilter = Format-SdnMacAddress -MacAddress $MacAddress + $netAdapters = $netAdapters | Where-Object { + $_.MacAddress -and (Format-SdnMacAddress -MacAddress $_.MacAddress) -eq $formattedFilter + } + } + + foreach ($adapter in $netAdapters) { + $adapterMac = if ($adapter.MacAddress) { Format-SdnMacAddress -MacAddress $adapter.MacAddress } else { $null } + $portId = if ($adapterMac) { $macToPort[$adapterMac] } else { $null } + $acls = if ($portId -and $aclByPort.ContainsKey($portId)) { $aclByPort[$portId] } else { @() } + + foreach ($acl in $acls) { + $aclObject = [PSCustomObject]@{ + VMName = $adapter.VMName + AdapterName = $adapter.Name + MacAddress = $adapter.MacAddress + IsManagementOS = $adapter.IsManagement + Direction = $acl.Direction + Action = $acl.Action + LocalAddress = $acl.LocalAddress + RemoteAddress = $acl.RemoteAddress + LocalPort = $acl.LocalPort + RemotePort = $acl.RemotePort + Protocol = $acl.Protocol + Weight = $acl.Weight + Stateful = $acl.IsStateful + IdleSessionTimeout = $acl.IdleSessionTimeout + IsolationID = $acl.IsolationID + } + [void]$results.Add($aclObject) + } + } + + return $results + } + catch { + $_ | Trace-Exception + $_ | Write-Error + } +} + +function Get-SdnVMNetworkAdapterIsolationCim { + <# + .SYNOPSIS + Retrieves isolation settings for virtual machine network adapters using CIM. + .DESCRIPTION + Uses direct CIM queries against root/virtualization/v2 to retrieve Msvm_EthernetSwitchPortIsolationSettingData, + providing significantly faster performance compared to Get-VMNetworkAdapterIsolation. + .PARAMETER VMName + Specifies the name of the virtual machine whose isolation settings are to be retrieved. + .PARAMETER MacAddress + Specifies the MAC address of the network adapter to filter results. + .PARAMETER All + Switch to indicate to get all virtual machine network adapter isolation settings. + .PARAMETER ManagementOS + Specifies the management operating system adapters. + .PARAMETER ComputerName + Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers. + .PARAMETER Credential + Specifies a user account that has permission to perform this action. The default is the current user. + .EXAMPLE + PS> Get-SdnVMNetworkAdapterIsolationCim -ManagementOS + .EXAMPLE + PS> Get-SdnVMNetworkAdapterIsolationCim -All + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [string]$VMName, + + [Parameter(Mandatory = $false)] + [string]$MacAddress, + + [Parameter(Mandatory = $false)] + [switch]$All, + + [Parameter(Mandatory = $false)] + [switch]$ManagementOS, + + [Parameter(Mandatory = $false)] + [System.String[]]$ComputerName, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential = [System.Management.Automation.PSCredential]::Empty + ) + + $cimParams = @{ + Namespace = 'root/virtualization/v2' + ErrorAction = 'Stop' + } + + if ($ComputerName) { + $sessionParams = @{ ComputerName = $ComputerName } + if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { + $sessionParams.Add('Credential', $Credential) + } + $cimSession = New-SdnCimSession @sessionParams + $cimParams.Add('CimSession', $cimSession) + } + + try { + $results = [System.Collections.ArrayList]::new() + + $portAllocations = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetPortAllocationSettingData' + $isolationSettings = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetSwitchPortIsolationSettingData' + + # Build lookup: port InstanceID -> isolation setting + $isolationByPort = @{} + foreach ($iso in $isolationSettings) { + $portPath = $iso.InstanceID -replace '/[^/]+$', '' + $isolationByPort[$portPath] = $iso + } + + # Build MAC -> port InstanceID lookup + $macToPort = @{} + foreach ($port in $portAllocations) { + if ($port.Address) { + $portMac = Format-SdnMacAddress -MacAddress $port.Address + $macToPort[$portMac] = $port.InstanceID + } + } + + # Get adapters + $adapterParams = @{} + if ($VMName) { $adapterParams.Add('VMName', $VMName) } + if ($All) { $adapterParams.Add('All', $true) } + if ($ManagementOS) { $adapterParams.Add('ManagementOS', $true) } + if ($ComputerName) { $adapterParams.Add('ComputerName', $ComputerName) } + if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { + $adapterParams.Add('Credential', $Credential) + } + + $netAdapters = Get-SdnVMNetworkAdapterCim @adapterParams + if ($MacAddress) { + $formattedFilter = Format-SdnMacAddress -MacAddress $MacAddress + $netAdapters = $netAdapters | Where-Object { + $_.MacAddress -and (Format-SdnMacAddress -MacAddress $_.MacAddress) -eq $formattedFilter + } + } + + foreach ($adapter in $netAdapters) { + $adapterMac = if ($adapter.MacAddress) { Format-SdnMacAddress -MacAddress $adapter.MacAddress } else { $null } + $portId = if ($adapterMac) { $macToPort[$adapterMac] } else { $null } + $iso = if ($portId -and $isolationByPort.ContainsKey($portId)) { $isolationByPort[$portId] } else { $null } + + $isoObject = [PSCustomObject]@{ + VMName = $adapter.VMName + AdapterName = $adapter.Name + MacAddress = $adapter.MacAddress + IsManagementOS = $adapter.IsManagement + IsolationMode = if ($iso) { $iso.IsolationMode } else { $null } + DefaultIsolationId = if ($iso) { $iso.DefaultIsolationId } else { $null } + AllowUntaggedTraffic = if ($iso) { $iso.AllowUntaggedTraffic } else { $null } + MultiTenantStack = if ($iso) { $iso.EnableMultiTenantStack } else { $null } + } + + [void]$results.Add($isoObject) + } + + return ($results | Sort-Object -Property AdapterName) + } + catch { + $_ | Trace-Exception + $_ | Write-Error + } +} + +function Get-SdnVMNetworkAdapterRoutingDomainCim { + <# + .SYNOPSIS + Retrieves routing domain mapping settings for virtual machine network adapters using CIM. + .DESCRIPTION + Uses direct CIM queries against root/virtualization/v2 to retrieve Msvm_EthernetSwitchPortRoutingDomainSettingData, + providing significantly faster performance compared to Get-VMNetworkAdapterRoutingDomainMapping. + .PARAMETER VMName + Specifies the name of the virtual machine whose routing domain settings are to be retrieved. + .PARAMETER MacAddress + Specifies the MAC address of the network adapter to filter results. + .PARAMETER All + Switch to indicate to get all virtual machine network adapter routing domain settings. + .PARAMETER ManagementOS + Specifies the management operating system adapters. + .PARAMETER ComputerName + Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers. + .PARAMETER Credential + Specifies a user account that has permission to perform this action. The default is the current user. + .EXAMPLE + PS> Get-SdnVMNetworkAdapterRoutingDomainCim -ManagementOS + .EXAMPLE + PS> Get-SdnVMNetworkAdapterRoutingDomainCim -All + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [string]$VMName, + + [Parameter(Mandatory = $false)] + [string]$MacAddress, + + [Parameter(Mandatory = $false)] + [switch]$All, + + [Parameter(Mandatory = $false)] + [switch]$ManagementOS, + + [Parameter(Mandatory = $false)] + [System.String[]]$ComputerName, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential = [System.Management.Automation.PSCredential]::Empty + ) + + $cimParams = @{ + Namespace = 'root/virtualization/v2' + ErrorAction = 'Stop' + } + + if ($ComputerName) { + $sessionParams = @{ ComputerName = $ComputerName } + if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { + $sessionParams.Add('Credential', $Credential) + } + $cimSession = New-SdnCimSession @sessionParams + $cimParams.Add('CimSession', $cimSession) + } + + try { + $results = [System.Collections.ArrayList]::new() + + $portAllocations = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetPortAllocationSettingData' + $routingSettings = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetSwitchPortRoutingDomainSettingData' + + # Build lookup: port InstanceID -> list of routing domain settings + $routingByPort = @{} + foreach ($rd in $routingSettings) { + $portPath = $rd.InstanceID -replace '/[^/]+$', '' + if (-not $routingByPort.ContainsKey($portPath)) { + $routingByPort[$portPath] = [System.Collections.ArrayList]::new() + } + [void]$routingByPort[$portPath].Add($rd) + } + + # Build MAC -> port InstanceID lookup + $macToPort = @{} + foreach ($port in $portAllocations) { + if ($port.Address) { + $portMac = Format-SdnMacAddress -MacAddress $port.Address + $macToPort[$portMac] = $port.InstanceID + } + } + + # Get adapters + $adapterParams = @{} + if ($VMName) { $adapterParams.Add('VMName', $VMName) } + if ($All) { $adapterParams.Add('All', $true) } + if ($ManagementOS) { $adapterParams.Add('ManagementOS', $true) } + if ($ComputerName) { $adapterParams.Add('ComputerName', $ComputerName) } + if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { + $adapterParams.Add('Credential', $Credential) + } + + $netAdapters = Get-SdnVMNetworkAdapterCim @adapterParams + if ($MacAddress) { + $formattedFilter = Format-SdnMacAddress -MacAddress $MacAddress + $netAdapters = $netAdapters | Where-Object { + $_.MacAddress -and (Format-SdnMacAddress -MacAddress $_.MacAddress) -eq $formattedFilter + } + } + + foreach ($adapter in $netAdapters) { + $adapterMac = if ($adapter.MacAddress) { Format-SdnMacAddress -MacAddress $adapter.MacAddress } else { $null } + $portId = if ($adapterMac) { $macToPort[$adapterMac] } else { $null } + $rdList = if ($portId -and $routingByPort.ContainsKey($portId)) { $routingByPort[$portId] } else { @() } + + foreach ($rd in $rdList) { + $rdObject = [PSCustomObject]@{ + VMName = $adapter.VMName + AdapterName = $adapter.Name + MacAddress = $adapter.MacAddress + IsManagementOS = $adapter.IsManagement + RoutingDomainGuid = $rd.RoutingDomainGuid + RoutingDomainName = $rd.RoutingDomainName + IsolationIdList = $rd.IsolationIdList + IsolationIdNameList = $rd.IsolationIdNameList + } + [void]$results.Add($rdObject) + } + } + + return $results + } + catch { + $_ | Trace-Exception + $_ | Write-Error + } +} + +function Get-SdnVMNetworkAdapterVlanCim { + <# + .SYNOPSIS + Retrieves VLAN settings for virtual machine network adapters using CIM. + .DESCRIPTION + Uses direct CIM queries against root/virtualization/v2 to retrieve Msvm_EthernetSwitchPortVlanSettingData, + providing significantly faster performance compared to Get-VMNetworkAdapterVlan. + .PARAMETER VMName + Specifies the name of the virtual machine whose VLAN settings are to be retrieved. + .PARAMETER MacAddress + Specifies the MAC address of the network adapter to filter results. + .PARAMETER All + Switch to indicate to get all virtual machine network adapter VLAN settings. + .PARAMETER ManagementOS + Specifies the management operating system adapters. + .PARAMETER ComputerName + Type the NetBIOS name, an IP address, or a fully qualified domain name of one or more remote computers. + .PARAMETER Credential + Specifies a user account that has permission to perform this action. The default is the current user. + .EXAMPLE + PS> Get-SdnVMNetworkAdapterVlanCim -ManagementOS + .EXAMPLE + PS> Get-SdnVMNetworkAdapterVlanCim -VMName 'VM01' + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [string]$VMName, + + [Parameter(Mandatory = $false)] + [string]$MacAddress, + + [Parameter(Mandatory = $false)] + [switch]$All, + + [Parameter(Mandatory = $false)] + [switch]$ManagementOS, + + [Parameter(Mandatory = $false)] + [System.String[]]$ComputerName, + + [Parameter(Mandatory = $false)] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential = [System.Management.Automation.PSCredential]::Empty + ) + + $cimParams = @{ + Namespace = 'root/virtualization/v2' + ErrorAction = 'Stop' + } + + if ($ComputerName) { + $sessionParams = @{ ComputerName = $ComputerName } + if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { + $sessionParams.Add('Credential', $Credential) + } + $cimSession = New-SdnCimSession @sessionParams + $cimParams.Add('CimSession', $cimSession) + } + + try { + $results = [System.Collections.ArrayList]::new() + + $portAllocations = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetPortAllocationSettingData' + $vlanSettings = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetSwitchPortVlanSettingData' + + # Build lookup: port InstanceID -> VLAN setting + $vlanByPort = @{} + foreach ($vlan in $vlanSettings) { + $portPath = $vlan.InstanceID -replace '/[^/]+$', '' + $vlanByPort[$portPath] = $vlan + } + + # Build MAC -> port InstanceID lookup + $macToPort = @{} + foreach ($port in $portAllocations) { + if ($port.Address) { + $portMac = Format-SdnMacAddress -MacAddress $port.Address + $macToPort[$portMac] = $port.InstanceID + } + } + + # Get adapters + $adapterParams = @{} + if ($VMName) { $adapterParams.Add('VMName', $VMName) } + if ($All) { $adapterParams.Add('All', $true) } + if ($ManagementOS) { $adapterParams.Add('ManagementOS', $true) } + if ($ComputerName) { $adapterParams.Add('ComputerName', $ComputerName) } + if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { + $adapterParams.Add('Credential', $Credential) + } + + $netAdapters = Get-SdnVMNetworkAdapterCim @adapterParams + if ($MacAddress) { + $formattedFilter = Format-SdnMacAddress -MacAddress $MacAddress + $netAdapters = $netAdapters | Where-Object { + $_.MacAddress -and (Format-SdnMacAddress -MacAddress $_.MacAddress) -eq $formattedFilter + } + } + + foreach ($adapter in $netAdapters) { + $adapterMac = if ($adapter.MacAddress) { Format-SdnMacAddress -MacAddress $adapter.MacAddress } else { $null } + $portId = if ($adapterMac) { $macToPort[$adapterMac] } else { $null } + $vlan = if ($portId -and $vlanByPort.ContainsKey($portId)) { $vlanByPort[$portId] } else { $null } + + # Map OperationMode integer to string + $operationMode = if ($vlan) { + switch ($vlan.OperationMode) { + 1 { 'Access' } + 2 { 'Trunk' } + 3 { 'Private' } + default { 'Untagged' } + } + } + else { 'Untagged' } + + $vlanObject = [PSCustomObject]@{ + VMName = $adapter.VMName + AdapterName = $adapter.Name + MacAddress = $adapter.MacAddress + IsManagementOS = $adapter.IsManagement + OperationMode = $operationMode + AccessVlanId = if ($vlan) { $vlan.AccessVlanId } else { 0 } + NativeVlanId = if ($vlan) { $vlan.NativeVlanId } else { 0 } + PrimaryVlanId = if ($vlan) { $vlan.PrimaryVlanId } else { 0 } + SecondaryVlanId = if ($vlan) { $vlan.SecondaryVlanId } else { 0 } + SecondaryVlanIdList = if ($vlan) { $vlan.SecondaryVlanIdList } else { $null } + PrivateVlanMode = if ($vlan) { $vlan.PvlanMode } else { $null } + PruneVlanIdArray = if ($vlan) { $vlan.PruneEnabledVlanIdArray } else { $null } + TrunkVlanIdArray = if ($vlan) { $vlan.TrunkVlanIdArray } else { $null } + AllowedVlanIdListString = if ($vlan -and $vlan.TrunkVlanIdArray) { ($vlan.TrunkVlanIdArray -join ',') } else { $null } + } + + [void]$results.Add($vlanObject) + } + + return ($results | Sort-Object -Property AdapterName) + } + catch { + $_ | Trace-Exception + $_ | Write-Error + } +} + function Get-SdnVMNetworkAdapterPortProfile { <# .SYNOPSIS @@ -4284,7 +4834,7 @@ function Repair-SdnVMNetworkAdapterPortProfile { $vmNetworkAdapters = Get-SdnVMNetworkAdapterPortProfile -VMName $VMName -ErrorAction Stop $currentPortProfileSettings = $vmNetworkAdapters | Where-Object {$_.MacAddress -eq $formattedMacAddress} - $currentVlanConfiguration = Get-SdnVMNetworkAdapter -VMName $VMName -MacAddress $formattedMacAddress -ErrorAction Stop | Get-VMNetworkAdapterVlan -ErrorAction Stop + $currentVlanConfiguration = Get-SdnVMNetworkAdapterVlanCim -VMName $VMName -MacAddress $formattedMacAddress -ErrorAction Stop } else { $repairPortProfileParams.Add('HyperVHost', $HyperVHost) @@ -4300,7 +4850,7 @@ function Repair-SdnVMNetworkAdapterPortProfile { $currentVlanConfiguration = Invoke-SdnCommand -ComputerName $HyperVHost -Credential $Credential -ScriptBlock { param($vmName, $macAddress) - return (Get-SdnVMNetworkAdapter -VMName $vmName -MacAddress $macAddress -ErrorAction Stop | Get-VMNetworkAdapterVlan -ErrorAction Stop) + return (Get-SdnVMNetworkAdapterVlanCim -VMName $vmName -MacAddress $macAddress -ErrorAction Stop) } -ArgumentList @($VMName, $formattedMacAddress) -ErrorAction Stop } if ($null -ieq $currentPortProfileSettings) { @@ -4353,7 +4903,7 @@ function Repair-SdnVMNetworkAdapterPortProfile { # SecondaryVlanIdList is only populated when PrivateVlanMode is Promiscuous. $privateVlanMode = [string]$vlanConfiguration.PrivateVlanMode $secondaryVlanDetails = if ($privateVlanMode -ieq 'Promiscuous') { - "SecondaryVlanIdList [{0}]" -f $vlanConfiguration.SecondaryVlanIdListString + "SecondaryVlanIdList [{0}]" -f ($vlanConfiguration.SecondaryVlanIdList -join ',') } else { "SecondaryVlanId [{0}]" -f $vlanConfiguration.SecondaryVlanId diff --git a/tests/offline/Server.Tests.ps1 b/tests/offline/Server.Tests.ps1 index ba7689ba..b9ccde6f 100644 --- a/tests/offline/Server.Tests.ps1 +++ b/tests/offline/Server.Tests.ps1 @@ -105,11 +105,11 @@ namespace SdnDiagnostics.PesterOffline { } } - Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Get-VMNetworkAdapterVlan' } -MockWith { + Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Get-SdnVMNetworkAdapterVlanCim' } -MockWith { return ([PSCustomObject]@{ - OperationMode = [SdnDiagnostics.PesterOffline.VMNetworkAdapterVlanMode]::Access + OperationMode = 'Access' AccessVlanId = 101 - } | ConvertTo-PesterRemoteObject) + }) } Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Set-VMNetworkAdapterVlan' } -MockWith { } @@ -138,13 +138,13 @@ namespace SdnDiagnostics.PesterOffline { } } - Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Get-VMNetworkAdapterVlan' } -MockWith { + Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Get-SdnVMNetworkAdapterVlanCim' } -MockWith { return ([PSCustomObject]@{ - OperationMode = [SdnDiagnostics.PesterOffline.VMNetworkAdapterVlanMode]::Trunk + OperationMode = 'Trunk' NativeVlanId = 0 AllowedVlanIdList = @(1..100) AllowedVlanIdListString = '1-100' - } | ConvertTo-PesterRemoteObject) + }) } Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Set-VMNetworkAdapterVlan' } -MockWith { } @@ -175,15 +175,15 @@ namespace SdnDiagnostics.PesterOffline { # when PrivateVlanMode is Promiscuous, SecondaryVlanId is not populated and the configured VLANs # are exposed via SecondaryVlanIdList / SecondaryVlanIdListString instead. - Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Get-VMNetworkAdapterVlan' } -MockWith { + Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Get-SdnVMNetworkAdapterVlanCim' } -MockWith { return ([PSCustomObject]@{ - OperationMode = [SdnDiagnostics.PesterOffline.VMNetworkAdapterVlanMode]::Private - PrivateVlanMode = [SdnDiagnostics.PesterOffline.VMNetworkAdapterPrivateVlanMode]::Promiscuous + OperationMode = 'Private' + PrivateVlanMode = 'Promiscuous' PrimaryVlanId = 10 SecondaryVlanId = 0 SecondaryVlanIdList = @(11, 12) SecondaryVlanIdListString = '11-12' - } | ConvertTo-PesterRemoteObject) + }) } Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Set-VMNetworkAdapterVlan' } -MockWith { } @@ -213,15 +213,15 @@ namespace SdnDiagnostics.PesterOffline { } # when PrivateVlanMode is Isolated, SecondaryVlanId is populated and SecondaryVlanIdList is null. - Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Get-VMNetworkAdapterVlan' } -MockWith { + Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Get-SdnVMNetworkAdapterVlanCim' } -MockWith { return ([PSCustomObject]@{ - OperationMode = [SdnDiagnostics.PesterOffline.VMNetworkAdapterVlanMode]::Private - PrivateVlanMode = [SdnDiagnostics.PesterOffline.VMNetworkAdapterPrivateVlanMode]::Isolated + OperationMode = 'Private' + PrivateVlanMode = 'Isolated' PrimaryVlanId = 10 SecondaryVlanId = 11 SecondaryVlanIdList = $null SecondaryVlanIdListString = $null - } | ConvertTo-PesterRemoteObject) + }) } Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Set-VMNetworkAdapterVlan' } -MockWith { } @@ -250,11 +250,11 @@ namespace SdnDiagnostics.PesterOffline { } } - Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Get-VMNetworkAdapterVlan' } -MockWith { + Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Get-SdnVMNetworkAdapterVlanCim' } -MockWith { return ([PSCustomObject]@{ - OperationMode = [SdnDiagnostics.PesterOffline.VMNetworkAdapterVlanMode]::Untagged + OperationMode = 'Untagged' AccessVlanId = 0 - } | ConvertTo-PesterRemoteObject) + }) } Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Set-VMNetworkAdapterVlan' } -MockWith { } @@ -284,11 +284,11 @@ namespace SdnDiagnostics.PesterOffline { } } - Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Get-VMNetworkAdapterVlan' } -MockWith { + Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Get-SdnVMNetworkAdapterVlanCim' } -MockWith { return ([PSCustomObject]@{ - OperationMode = [SdnDiagnostics.PesterOffline.VMNetworkAdapterVlanMode]::Untagged + OperationMode = 'Untagged' AccessVlanId = 0 - } | ConvertTo-PesterRemoteObject) + }) } Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Set-VMNetworkAdapterVlan' } -MockWith { } @@ -318,11 +318,11 @@ namespace SdnDiagnostics.PesterOffline { } } - Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Get-VMNetworkAdapterVlan' } -MockWith { + Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Get-SdnVMNetworkAdapterVlanCim' } -MockWith { return ([PSCustomObject]@{ - OperationMode = [SdnDiagnostics.PesterOffline.VMNetworkAdapterVlanMode]::Access + OperationMode = 'Access' AccessVlanId = 200 - } | ConvertTo-PesterRemoteObject) + }) } Mock Invoke-SdnCommand -ParameterFilter { $ScriptBlock.ToString() -match 'Set-VMNetworkAdapterVlan' } -MockWith { } @@ -366,9 +366,9 @@ namespace SdnDiagnostics.PesterOffline { # the local host code path receives the live object from Get-VMNetworkAdapterVlan, so the enum # is returned as-is rather than being deserialized as its underlying integer value. - Mock Get-VMNetworkAdapterVlan { + Mock Get-SdnVMNetworkAdapterVlanCim { return [PSCustomObject]@{ - OperationMode = [SdnDiagnostics.PesterOffline.VMNetworkAdapterVlanMode]::Access + OperationMode = 'Access' AccessVlanId = 101 } } @@ -409,9 +409,9 @@ namespace SdnDiagnostics.PesterOffline { } } - Mock Get-VMNetworkAdapterVlan { + Mock Get-SdnVMNetworkAdapterVlanCim { return [PSCustomObject]@{ - OperationMode = [SdnDiagnostics.PesterOffline.VMNetworkAdapterVlanMode]::Untagged + OperationMode = 'Untagged' AccessVlanId = 0 } } @@ -764,3 +764,189 @@ Describe 'Server - Get-SdnVMNetworkAdapterPortProfile (CIM)' { } } } + +Describe 'Server - Get-SdnVMNetworkAdapterVlanCim' { + + It "Returns VLAN settings with OperationMode as string" { + InModuleScope SdnDiag.Server { + Mock Get-CimInstance { + switch ($ClassName) { + 'Msvm_EthernetPortAllocationSettingData' { + return @([PSCustomObject]@{ + Address = '001DD8070001' + InstanceID = 'port001' + }) + } + 'Msvm_EthernetSwitchPortVlanSettingData' { + return @([PSCustomObject]@{ + InstanceID = 'port001/vlan' + OperationMode = 1 + AccessVlanId = 100 + NativeVlanId = 0 + PrimaryVlanId = 0 + SecondaryVlanId = 0 + SecondaryVlanIdList = $null + PvlanMode = $null + PruneEnabledVlanIdArray = $null + TrunkVlanIdArray = $null + }) + } + default { return @() } + } + } + Mock Get-SdnVMNetworkAdapterCim { + return @([PSCustomObject]@{ + Name = 'Network Adapter'; MacAddress = '001DD8070001'; VMName = 'DVLAB-VM01'; IsManagement = $false + }) + } + Mock New-SdnCimSession { } + Mock Format-SdnMacAddress { return $MacAddress.ToUpper() -replace '-','' } + + $result = Get-SdnVMNetworkAdapterVlanCim -VMName 'DVLAB-VM01' + $result | Should -Not -BeNullOrEmpty + $result[0].OperationMode | Should -Be 'Access' + $result[0].AccessVlanId | Should -Be 100 + $result[0].VMName | Should -Be 'DVLAB-VM01' + } + } + + It "Returns Untagged when no VLAN setting exists for adapter" { + InModuleScope SdnDiag.Server { + Mock Get-CimInstance { return @() } + Mock Get-SdnVMNetworkAdapterCim { + return @([PSCustomObject]@{ + Name = 'Network Adapter'; MacAddress = '001DD8070001'; VMName = 'DVLAB-VM01'; IsManagement = $false + }) + } + Mock New-SdnCimSession { } + Mock Format-SdnMacAddress { return $MacAddress.ToUpper() -replace '-','' } + + $result = Get-SdnVMNetworkAdapterVlanCim -All + $result | Should -Not -BeNullOrEmpty + $result[0].OperationMode | Should -Be 'Untagged' + } + } +} + +Describe 'Server - Get-SdnVMNetworkAdapterIsolationCim' { + + It "Returns isolation settings for adapters" { + InModuleScope SdnDiag.Server { + Mock Get-CimInstance { + switch ($ClassName) { + 'Msvm_EthernetPortAllocationSettingData' { + return @([PSCustomObject]@{ Address = '001DD8070001'; InstanceID = 'port001' }) + } + 'Msvm_EthernetSwitchPortIsolationSettingData' { + return @([PSCustomObject]@{ + InstanceID = 'port001/isolation' + IsolationMode = 1 + DefaultIsolationId = 4096 + AllowUntaggedTraffic = $true + EnableMultiTenantStack = $false + }) + } + default { return @() } + } + } + Mock Get-SdnVMNetworkAdapterCim { + return @([PSCustomObject]@{ + Name = 'Network Adapter'; MacAddress = '001DD8070001'; VMName = 'DVLAB-VM01'; IsManagement = $false + }) + } + Mock New-SdnCimSession { } + Mock Format-SdnMacAddress { return $MacAddress.ToUpper() -replace '-','' } + + $result = Get-SdnVMNetworkAdapterIsolationCim -All + $result | Should -Not -BeNullOrEmpty + $result[0].IsolationMode | Should -Be 1 + $result[0].DefaultIsolationId | Should -Be 4096 + } + } +} + +Describe 'Server - Get-SdnVMNetworkAdapterExtendedAclCim' { + + It "Returns extended ACL entries per adapter" { + InModuleScope SdnDiag.Server { + Mock Get-CimInstance { + switch ($ClassName) { + 'Msvm_EthernetPortAllocationSettingData' { + return @([PSCustomObject]@{ Address = '001DD8070001'; InstanceID = 'port001' }) + } + 'Msvm_EthernetSwitchPortExtendedAclSettingData' { + return @( + [PSCustomObject]@{ + InstanceID = 'port001/acl1' + Direction = 1 + Action = 1 + LocalAddress = '*' + RemoteAddress = '10.0.0.0/8' + LocalPort = '*' + RemotePort = '443' + Protocol = 'TCP' + Weight = 100 + IsStateful = $true + IdleSessionTimeout = 0 + IsolationID = 0 + } + ) + } + default { return @() } + } + } + Mock Get-SdnVMNetworkAdapterCim { + return @([PSCustomObject]@{ + Name = 'Network Adapter'; MacAddress = '001DD8070001'; VMName = 'DVLAB-VM01'; IsManagement = $false + }) + } + Mock New-SdnCimSession { } + Mock Format-SdnMacAddress { return $MacAddress.ToUpper() -replace '-','' } + + $result = Get-SdnVMNetworkAdapterExtendedAclCim -All + $result | Should -Not -BeNullOrEmpty + $result[0].Direction | Should -Be 1 + $result[0].RemoteAddress | Should -Be '10.0.0.0/8' + $result[0].Protocol | Should -Be 'TCP' + } + } +} + +Describe 'Server - Get-SdnVMNetworkAdapterRoutingDomainCim' { + + It "Returns routing domain entries per adapter" { + InModuleScope SdnDiag.Server { + Mock Get-CimInstance { + switch ($ClassName) { + 'Msvm_EthernetPortAllocationSettingData' { + return @([PSCustomObject]@{ Address = '001DD8070001'; InstanceID = 'port001' }) + } + 'Msvm_EthernetSwitchPortRoutingDomainSettingData' { + return @( + [PSCustomObject]@{ + InstanceID = 'port001/rd1' + RoutingDomainGuid = '{11111111-2222-3333-4444-555555555555}' + RoutingDomainName = 'VNET-001' + IsolationIdList = @(4096, 4097) + IsolationIdNameList = @('Subnet-1', 'Subnet-2') + } + ) + } + default { return @() } + } + } + Mock Get-SdnVMNetworkAdapterCim { + return @([PSCustomObject]@{ + Name = 'Network Adapter'; MacAddress = '001DD8070001'; VMName = 'DVLAB-VM01'; IsManagement = $false + }) + } + Mock New-SdnCimSession { } + Mock Format-SdnMacAddress { return $MacAddress.ToUpper() -replace '-','' } + + $result = Get-SdnVMNetworkAdapterRoutingDomainCim -All + $result | Should -Not -BeNullOrEmpty + $result[0].RoutingDomainName | Should -Be 'VNET-001' + $result[0].IsolationIdList | Should -Contain 4096 + } + } +} From 3a163e711769a58c42dfe4679658f663a63b01e6 Mon Sep 17 00:00:00 2001 From: Adam Rudell Date: Thu, 13 Aug 2026 16:33:25 -0500 Subject: [PATCH 5/6] Replace Get-VMNetworkAdapter in per-VM loop with CIM port setting functions Drop legacy AclList export. Use Get-SdnVMNetworkAdapterExtendedAclCim, IsolationCim, RoutingDomainCim, and VlanCim filtered by VMName instead of the slow Get-VMNetworkAdapter per-VM call. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 220ab25a-0e38-4217-8e3c-e0a2e22fd7d9 --- src/modules/SdnDiag.Server.psm1 | 36 +++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/modules/SdnDiag.Server.psm1 b/src/modules/SdnDiag.Server.psm1 index 73362474..3bbe01d6 100644 --- a/src/modules/SdnDiag.Server.psm1 +++ b/src/modules/SdnDiag.Server.psm1 @@ -516,22 +516,28 @@ function Get-ServerConfigState { } } - # collect per-VM adapter diagnostic details using Hyper-V cmdlets + # collect per-VM adapter diagnostic details using CIM try { - $hvAdapters = Get-VMNetworkAdapter -VMName $vm.Name -ErrorAction SilentlyContinue - foreach ($hvAdapter in $hvAdapters) { - $prefix = (Format-SdnMacAddress -MacAddress $hvAdapter.MacAddress) - $hvAdapter.AclList | Remove-PropertiesFromObject -PropertiesToRemove 'ParentAdapter' | - Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_AclList' -FileType txt -Format List - $hvAdapter.ExtendedAclList | Remove-PropertiesFromObject -PropertiesToRemove 'ParentAdapter','CimSession' | - Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_ExtendedAclList' -FileType txt -Format List - $hvAdapter.IsolationSetting | Remove-PropertiesFromObject -PropertiesToRemove 'ParentAdapter','CimSession' | - Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_IsolationSetting' -FileType txt -Format List - $hvAdapter.RoutingDomainList | Remove-PropertiesFromObject -PropertiesToRemove 'ParentAdapter','CimSession' | - Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_RoutingDomainList' -FileType txt -Format List - $hvAdapter.VlanSetting | Remove-PropertiesFromObject -PropertiesToRemove 'ParentAdapter','CimSession' | - Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_VlanSetting' -FileType txt -Format List - } + Get-SdnVMNetworkAdapterExtendedAclCim -VMName $vm.Name | + Group-Object -Property MacAddress | ForEach-Object { + $prefix = $_.Name + $_.Group | Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_ExtendedAclList' -FileType txt -Format List + } + Get-SdnVMNetworkAdapterIsolationCim -VMName $vm.Name | + ForEach-Object { + $prefix = (Format-SdnMacAddress -MacAddress $_.MacAddress) + $_ | Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_IsolationSetting' -FileType txt -Format List + } + Get-SdnVMNetworkAdapterRoutingDomainCim -VMName $vm.Name | + Group-Object -Property MacAddress | ForEach-Object { + $prefix = $_.Name + $_.Group | Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_RoutingDomainList' -FileType txt -Format List + } + Get-SdnVMNetworkAdapterVlanCim -VMName $vm.Name | + ForEach-Object { + $prefix = (Format-SdnMacAddress -MacAddress $_.MacAddress) + $_ | Export-ObjectToFile -FilePath $vmDir.FullName -Prefix $prefix -Name 'Get-VM_VlanSetting' -FileType txt -Format List + } } catch { "Failed to enumerate detailed VMNetworkAdapter settings for VM {0}" -f $vm.Name | Trace-Output -Level:Warning From 5817adecf55807c9495e320c30a5647535029c4d Mon Sep 17 00:00:00 2001 From: Adam Rudell Date: Fri, 14 Aug 2026 12:30:50 -0500 Subject: [PATCH 6/6] Address PR review round 2: fix InstanceID join, N+1 round trips, WQL injection, CIM write path - Fix InstanceID join pattern: use backslash separator (\C\GUID) matching real Hyper-V format instead of forward-slash stripping across all 6 port setting/profile locations - Eliminate N+1 round trips in Get-SdnVMCim: bulk-query VirtualSystemSettingData and SyntheticEthernetPortSettingData, join locally by VM GUID - Escape WQL injection: apostrophes in switch/VM names no longer break WQL queries (3 locations) - Fix SwitchType/BandwidthPercentage: renamed to accurate CIM field names (IOVPreferred, MaxIOVOffloads) - Fix CimSession array support: Get-SdnCimAssociatedInstance now accepts CimSession[] - Fix PortName semantic: use ElementName from port allocation for VFP compatibility - Fix Set-CimInstance on read-only snapshots: use Hyper-V cmdlet pipeline (Get/Set-VMSwitchExtensionPortFeature) instead of Set-CimInstance for modifying port profiles - Update test mock data to use proper Hyper-V InstanceID format (backslash-delimited) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 220ab25a-0e38-4217-8e3c-e0a2e22fd7d9 --- src/modules/SdnDiag.Server.psm1 | 91 ++++++++++++++++++++++----------- tests/offline/Server.Tests.ps1 | 21 ++++---- 2 files changed, 73 insertions(+), 39 deletions(-) diff --git a/src/modules/SdnDiag.Server.psm1 b/src/modules/SdnDiag.Server.psm1 index 3bbe01d6..a3cab439 100644 --- a/src/modules/SdnDiag.Server.psm1 +++ b/src/modules/SdnDiag.Server.psm1 @@ -2645,7 +2645,7 @@ function Get-SdnCimAssociatedInstance { [string]$Namespace, [Parameter(Mandatory = $false)] - [Microsoft.Management.Infrastructure.CimSession]$CimSession + [Microsoft.Management.Infrastructure.CimSession[]]$CimSession ) $params = @{ @@ -2722,7 +2722,8 @@ function Get-SdnVMSwitchCim { try { $filter = $null if ($Name) { - $filter = "ElementName = '$Name'" + $escapedName = $Name -replace "'", "''" + $filter = "ElementName = '$escapedName'" } $switchParams = @{} @@ -2738,9 +2739,9 @@ function Get-SdnVMSwitchCim { $switchObject = [PSCustomObject]@{ Name = $sw.ElementName SwitchId = $sw.Name - SwitchType = $sw.IOVPreferred + IOVPreferred = $sw.IOVPreferred Notes = $sw.Notes - BandwidthPercentage = $sw.MaxIOVOffloads + MaxIOVOffloads = $sw.MaxIOVOffloads InstallDate = $sw.InstallDate } @@ -2918,7 +2919,8 @@ function Get-SdnVMNetworkAdapterCim { $filter = $null if ($VMName) { # Get the VM's CIM object to find its associated adapters - $vmFilter = "ElementName = '$VMName' AND Caption = 'Virtual Machine'" + $escapedVMName = $VMName -replace "'", "''" + $vmFilter = "ElementName = '$escapedVMName' AND Caption = 'Virtual Machine'" $vmCim = Get-CimInstance @cimParams -ClassName 'Msvm_ComputerSystem' -Filter $vmFilter if ($null -eq $vmCim) { "Unable to locate virtual machine with name '$VMName'" | Trace-Output -Level:Warning @@ -3116,12 +3118,35 @@ function Get-SdnVMCim { try { $filter = "Caption = 'Virtual Machine'" if ($VMName) { - $filter += " AND ElementName = '$VMName'" + $escapedVMName = $VMName -replace "'", "''" + $filter += " AND ElementName = '$escapedVMName'" } $vmSystems = Get-CimInstance @cimParams -ClassName 'Msvm_ComputerSystem' -Filter $filter $results = [System.Collections.ArrayList]::new() + # Bulk-query all VM settings and adapters to avoid N+1 round trips + $allVmSettings = Get-CimInstance @cimParams -ClassName 'Msvm_VirtualSystemSettingData' -Filter "VirtualSystemType = 'Microsoft:Hyper-V:System:Realized'" + $allSyntheticAdapters = Get-CimInstance @cimParams -ClassName 'Msvm_SyntheticEthernetPortSettingData' + + # Build lookup: VM GUID -> settings + $settingsByVmGuid = @{} + foreach ($setting in $allVmSettings) { + if ($setting.InstanceID -match '^Microsoft:(.+)$') { + $settingsByVmGuid[$Matches[1]] = $setting + } + } + + # Build lookup: VM GUID -> list of adapters (InstanceID format: Microsoft:VMGUID\AdapterGUID) + $adaptersByVmGuid = @{} + foreach ($adapter in $allSyntheticAdapters) { + $vmGuid = ($adapter.InstanceID -split '\\')[0] -replace '^Microsoft:', '' + if (-not $adaptersByVmGuid.ContainsKey($vmGuid)) { + $adaptersByVmGuid[$vmGuid] = [System.Collections.ArrayList]::new() + } + [void]$adaptersByVmGuid[$vmGuid].Add($adapter) + } + foreach ($vm in $vmSystems) { # Map EnabledState to friendly status $state = switch ($vm.EnabledState) { @@ -3140,15 +3165,8 @@ function Get-SdnVMCim { default { 'Unknown' } } - # Get the associated settings to retrieve additional VM details - $vmSettings = Get-SdnCimAssociatedInstance -InputObject $vm -ResultClassName 'Msvm_VirtualSystemSettingData' @cimParams | - Where-Object { $_.VirtualSystemType -eq 'Microsoft:Hyper-V:System:Realized' } - - # Get network adapters associated with this VM - $networkAdapters = @() - if ($vmSettings) { - $networkAdapters = Get-SdnCimAssociatedInstance -InputObject $vmSettings -ResultClassName 'Msvm_SyntheticEthernetPortSettingData' @cimParams - } + # Get network adapters from the bulk lookup + $networkAdapters = if ($adaptersByVmGuid.ContainsKey($vm.Name)) { $adaptersByVmGuid[$vm.Name] } else { @() } $vmObject = [PSCustomObject]@{ Name = $vm.ElementName @@ -3252,7 +3270,7 @@ function Get-SdnVMNetworkAdapterExtendedAclCim { # Build lookup: port InstanceID -> list of ACL settings $aclByPort = @{} foreach ($acl in $aclSettings) { - $portPath = $acl.InstanceID -replace '/[^/]+$', '' + $portPath = $acl.InstanceID -replace '\\[^\\]+\\[^\\]+$', '' if (-not $aclByPort.ContainsKey($portPath)) { $aclByPort[$portPath] = [System.Collections.ArrayList]::new() } @@ -3392,7 +3410,7 @@ function Get-SdnVMNetworkAdapterIsolationCim { # Build lookup: port InstanceID -> isolation setting $isolationByPort = @{} foreach ($iso in $isolationSettings) { - $portPath = $iso.InstanceID -replace '/[^/]+$', '' + $portPath = $iso.InstanceID -replace '\\[^\\]+\\[^\\]+$', '' $isolationByPort[$portPath] = $iso } @@ -3521,7 +3539,7 @@ function Get-SdnVMNetworkAdapterRoutingDomainCim { # Build lookup: port InstanceID -> list of routing domain settings $routingByPort = @{} foreach ($rd in $routingSettings) { - $portPath = $rd.InstanceID -replace '/[^/]+$', '' + $portPath = $rd.InstanceID -replace '\\[^\\]+\\[^\\]+$', '' if (-not $routingByPort.ContainsKey($portPath)) { $routingByPort[$portPath] = [System.Collections.ArrayList]::new() } @@ -3654,7 +3672,7 @@ function Get-SdnVMNetworkAdapterVlanCim { # Build lookup: port InstanceID -> VLAN setting $vlanByPort = @{} foreach ($vlan in $vlanSettings) { - $portPath = $vlan.InstanceID -replace '/[^/]+$', '' + $portPath = $vlan.InstanceID -replace '\\[^\\]+\\[^\\]+$', '' $vlanByPort[$portPath] = $vlan } @@ -3806,7 +3824,7 @@ function Get-SdnVMNetworkAdapterPortProfile { # Build lookup of profile settings by port path $profileLookup = @{} foreach ($profile in $profileSettings) { - $portPath = $profile.InstanceID -replace '/[^/]+$', '' + $portPath = $profile.InstanceID -replace '\\[^\\]+\\[^\\]+$', '' $profileLookup[$portPath] = $profile } @@ -3871,9 +3889,9 @@ function Get-SdnVMNetworkAdapterPortProfile { } } - # Get the port name from the allocation - if ($matchedPort.InstanceID) { - $object.PortName = $matchedPort.InstanceID + # Get the port name from the allocation's Name property (VFP device ID) + if ($matchedPort.ElementName) { + $object.PortName = $matchedPort.ElementName } } @@ -4108,7 +4126,7 @@ function Set-SdnVMNetworkAdapterPortProfile { $existingProfile = $null $portInstancePath = $matchedPort.InstanceID foreach ($profile in $profileSettings) { - $profilePortPath = $profile.InstanceID -replace '/[^/]+$', '' + $profilePortPath = $profile.InstanceID -replace '\\[^\\]+\\[^\\]+$', '' if ($profilePortPath -eq $portInstancePath) { $existingProfile = $profile break @@ -4118,11 +4136,26 @@ function Set-SdnVMNetworkAdapterPortProfile { if ($existingProfile) { "Current Settings: ProfileId [{0}] ProfileData [{1}]" -f $existingProfile.ProfileId, $existingProfile.ProfileData | Trace-Output - # Update existing profile settings via CIM - $existingProfile.ProfileId = $ProfileId.ToString("B") - $existingProfile.ProfileData = $ProfileData - $existingProfile.VendorId = $vendorId.ToString("B") - Set-CimInstance -InputObject $existingProfile -ErrorAction Stop + # CIM instances from Get-CimInstance are read-only snapshots. + # Use Hyper-V cmdlet pipeline for modifying existing port profile settings. + if ($null -eq (Get-Module -Name Hyper-V)) { + Import-Module -Name Hyper-V -Force -ErrorAction Stop + } + + $fullVmNic = if ($HostVmNic) { + Get-VMNetworkAdapter -ManagementOS | Where-Object { (Format-SdnMacAddress -MacAddress $_.MacAddress) -eq $formattedMac } + } + else { + Get-VMNetworkAdapter -VMName $VMName | Where-Object { (Format-SdnMacAddress -MacAddress $_.MacAddress) -eq $formattedMac } + } + + $portFeature = Get-VMSwitchExtensionPortFeature -VMNetworkAdapter $fullVmNic -FeatureId "9940cd46-8b06-43bb-b9d5-93d50381fd56" + if ($portFeature) { + $portFeature.SettingData.ProfileId = $ProfileId.ToString("B") + $portFeature.SettingData.ProfileData = $ProfileData + $portFeature.SettingData.VendorId = $vendorId.ToString("B") + Set-VMSwitchExtensionPortFeature -VMSwitchExtensionFeature $portFeature -VMNetworkAdapter $fullVmNic + } } else { # Create new port profile settings diff --git a/tests/offline/Server.Tests.ps1 b/tests/offline/Server.Tests.ps1 index b9ccde6f..f8cc3e39 100644 --- a/tests/offline/Server.Tests.ps1 +++ b/tests/offline/Server.Tests.ps1 @@ -714,14 +714,15 @@ Describe 'Server - Get-SdnVMNetworkAdapterPortProfile (CIM)' { return @( [PSCustomObject]@{ Address = '001DD8070001' - InstanceID = 'Microsoft:AAAAAAAA-1111-2222-3333-444444444444\BBBBBBBB-5555-6666-7777-888888888888\port001' + InstanceID = 'Microsoft:AAAAAAAA-1111-2222-3333-444444444444\BBBBBBBB-5555-6666-7777-888888888888' + ElementName = 'port001' } ) } 'Msvm_EthernetSwitchPortProfileSettingData' { return @( [PSCustomObject]@{ - InstanceID = 'Microsoft:AAAAAAAA-1111-2222-3333-444444444444\BBBBBBBB-5555-6666-7777-888888888888\port001/profile' + InstanceID = 'Microsoft:AAAAAAAA-1111-2222-3333-444444444444\BBBBBBBB-5555-6666-7777-888888888888\C\PPPP0001-0000-0000-0000-000000000001' ProfileId = '{11111111-2222-3333-4444-555555555555}' ProfileData = 1 VendorId = '{1FA41B39-B444-4E43-B35A-E1F7985FD548}' @@ -774,12 +775,12 @@ Describe 'Server - Get-SdnVMNetworkAdapterVlanCim' { 'Msvm_EthernetPortAllocationSettingData' { return @([PSCustomObject]@{ Address = '001DD8070001' - InstanceID = 'port001' + InstanceID = 'Microsoft:VMGUID\PortGUID' }) } 'Msvm_EthernetSwitchPortVlanSettingData' { return @([PSCustomObject]@{ - InstanceID = 'port001/vlan' + InstanceID = 'Microsoft:VMGUID\PortGUID\C\VlanGUID' OperationMode = 1 AccessVlanId = 100 NativeVlanId = 0 @@ -835,11 +836,11 @@ Describe 'Server - Get-SdnVMNetworkAdapterIsolationCim' { Mock Get-CimInstance { switch ($ClassName) { 'Msvm_EthernetPortAllocationSettingData' { - return @([PSCustomObject]@{ Address = '001DD8070001'; InstanceID = 'port001' }) + return @([PSCustomObject]@{ Address = '001DD8070001'; InstanceID = 'Microsoft:VMGUID\PortGUID' }) } 'Msvm_EthernetSwitchPortIsolationSettingData' { return @([PSCustomObject]@{ - InstanceID = 'port001/isolation' + InstanceID = 'Microsoft:VMGUID\PortGUID\C\IsoGUID' IsolationMode = 1 DefaultIsolationId = 4096 AllowUntaggedTraffic = $true @@ -872,12 +873,12 @@ Describe 'Server - Get-SdnVMNetworkAdapterExtendedAclCim' { Mock Get-CimInstance { switch ($ClassName) { 'Msvm_EthernetPortAllocationSettingData' { - return @([PSCustomObject]@{ Address = '001DD8070001'; InstanceID = 'port001' }) + return @([PSCustomObject]@{ Address = '001DD8070001'; InstanceID = 'Microsoft:VMGUID\PortGUID' }) } 'Msvm_EthernetSwitchPortExtendedAclSettingData' { return @( [PSCustomObject]@{ - InstanceID = 'port001/acl1' + InstanceID = 'Microsoft:VMGUID\PortGUID\C\AclGUID' Direction = 1 Action = 1 LocalAddress = '*' @@ -919,12 +920,12 @@ Describe 'Server - Get-SdnVMNetworkAdapterRoutingDomainCim' { Mock Get-CimInstance { switch ($ClassName) { 'Msvm_EthernetPortAllocationSettingData' { - return @([PSCustomObject]@{ Address = '001DD8070001'; InstanceID = 'port001' }) + return @([PSCustomObject]@{ Address = '001DD8070001'; InstanceID = 'Microsoft:VMGUID\PortGUID' }) } 'Msvm_EthernetSwitchPortRoutingDomainSettingData' { return @( [PSCustomObject]@{ - InstanceID = 'port001/rd1' + InstanceID = 'Microsoft:VMGUID\PortGUID\C\RdGUID' RoutingDomainGuid = '{11111111-2222-3333-4444-555555555555}' RoutingDomainName = 'VNET-001' IsolationIdList = @(4096, 4097)