diff --git a/src/SdnDiagnostics.psd1 b/src/SdnDiagnostics.psd1 index 453eccdf..04fe4772 100644 --- a/src/SdnDiagnostics.psd1 +++ b/src/SdnDiagnostics.psd1 @@ -126,8 +126,15 @@ 'Get-SdnVipConfig', 'Get-SdnVfpVmSwitchPort', 'Get-SdnVMNetworkAdapter', + 'Get-SdnVMNetworkAdapterCim', + 'Get-SdnVMNetworkAdapterExtendedAclCim', + 'Get-SdnVMNetworkAdapterIsolationCim', 'Get-SdnVMNetworkAdapterPortProfile', + 'Get-SdnVMNetworkAdapterRoutingDomainCim', + 'Get-SdnVMNetworkAdapterVlanCim', + '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 4d08437d..a3cab439 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,44 +499,66 @@ 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 } } + + # collect per-VM adapter diagnostic details using CIM + try { + 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 + } } } - # 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-SdnVMNetworkAdapterCim -ManagementOS | Export-ObjectToFile -FilePath $outDir -Name 'Get-VMNetworkAdapter_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 @@ -2600,164 +2622,133 @@ function Get-SdnVMNetworkAdapter { } } -function Get-SdnVMNetworkAdapterPortProfile { + +function Get-SdnCimAssociatedInstance { <# .SYNOPSIS - Retrieves the port profile applied to the virtual machine network interfaces. - .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. - .EXAMPLE - Get-SdnVMNetworkAdapterPortProfile -VMName 'VM01' - .EXAMPLE - Get-SdnVMNetworkAdapterPortProfile -All + 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, ParameterSetName = 'VM')] - [System.String]$VMName, + [Parameter(Mandatory = $true)] + $InputObject, - [Parameter(Mandatory = $false, ParameterSetName = 'VM')] - [Parameter(Mandatory = $false, ParameterSetName = 'Management')] - [System.String]$Name, + [Parameter(Mandatory = $false)] + [string]$ResultClassName, - [Parameter(Mandatory = $true, ParameterSetName = 'All')] - [Switch]$All, + [Parameter(Mandatory = $false)] + [string]$Namespace, - [Parameter(Mandatory = $true, ParameterSetName = 'Management')] - [switch]$ManagementOS + [Parameter(Mandatory = $false)] + [Microsoft.Management.Infrastructure.CimSession[]]$CimSession ) - [System.Guid]$portProfileFeatureId = "9940cd46-8b06-43bb-b9d5-93d50381fd56" - $array = @() - - try { - $netAdapters = Get-SdnVMNetworkAdapter @PSBoundParameters - foreach ($adapter in $netAdapters) { - $object = [VMNetAdapterPortProfile]@{ - 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 - } - - # 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 - } - - $array += $object - } - - return ($array | Sort-Object -Property Name) - } - catch { - $_ | Trace-Exception - $_ | Write-Error + $params = @{ + InputObject = $InputObject } + if ($ResultClassName) { $params['ResultClassName'] = $ResultClassName } + if ($Namespace) { $params['Namespace'] = $Namespace } + if ($CimSession) { $params['CimSession'] = $CimSession } + + return (Get-CimAssociatedInstance @params) } -function New-SdnServerCertificate { +function Get-SdnVMSwitchCim { <# .SYNOPSIS - Generate new self-signed certificate to be used by the Hyper-V host and distributes to the Network Controller(s) within the environment. - .PARAMETER NotAfter - Specifies the date and time, as a DateTime object, that the certificate expires. To obtain a DateTime object, use the Get-Date cmdlet. The default value for this parameter is one year after the certificate was created. - .PARAMETER Path - Specifies the file path location where a .cer file is exported automatically. - .PARAMETER FabricDetails - The EnvironmentInfo derived from Get-SdnInfrastructureInfo. + 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 + 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 - New-SdnServerCertificate -NotAfter (Get-Date).AddYears(1) -FabricDetails $Global:SdnDiagnostics.EnvironmentInfo + PS> Get-SdnVMSwitchCim + .EXAMPLE + PS> Get-SdnVMSwitchCim -Name 'ConvergedSwitch' + .EXAMPLE + PS> Get-SdnVMSwitchCim -ComputerName 'Server01','Server02' -Credential (Get-Credential) #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] - [datetime]$NotAfter = (Get-Date).AddYears(3), + [string]$Name, [Parameter(Mandatory = $false)] - [System.String]$Path = "$(Get-WorkingDirectory)\ServerCert_{0}" -f (Get-FormattedDateTimeUTC), + [System.String[]]$ComputerName, [Parameter(Mandatory = $false)] - [SdnFabricInfrastructure]$FabricDetails, - [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] - $Credential = [System.Management.Automation.PSCredential]::Empty + $Credential = [System.Management.Automation.PSCredential]::Empty, + + [Parameter(Mandatory = $false)] + [Microsoft.Management.Infrastructure.CimSession[]]$CimSession ) - Confirm-IsServer # ensure that the module is running on a Server - Confirm-IsAdmin # ensure that the module is running as local administrator + $cimNamespace = 'root/virtualization/v2' + $cimParams = @{ + Namespace = $cimNamespace + ErrorAction = 'Stop' + } - try { - if (-NOT (Test-Path -Path $Path -PathType Container)) { - "Creating directory {0}" -f $Path | Trace-Output - $CertPath = New-Item -Path $Path -ItemType Directory -Force + if ($CimSession) { + $cimParams.Add('CimSession', $CimSession) + } + elseif ($ComputerName) { + $sessionParams = @{ + ComputerName = $ComputerName } - else { - $CertPath = Get-Item -Path $Path + if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { + $sessionParams.Add('Credential', $Credential) } - $serverCert = Get-ItemPropertyValue -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\NcHostAgent\Parameters' -Name 'HostAgentCertificateCName' -ErrorAction Stop - $subjectName = "CN={0}" -f $serverCert - - # check if there is a certificate present for Azure Stack Certification Authority for Azure Local systems - # if so, we will not generate a new certificate as they should be leveraging the certificate from the Azure Stack CA - if ($Global:SdnDiagnostics.Config.Mode -ieq 'AzureStackHCI') { - $azStackHCICertificates = Get-SdnServerCertificate -NetworkControllerOid -ErrorAction Ignore - if ($azStackHCICertificates) { - $azStackCertAuthorityCerts = $azStackHCICertificates | Where-Object { $_.Issuer -ieq 'CN=AzureStackCertificationAuthority' } - if ($azStackCertAuthorityCerts) { - # pick the most recent certificate based on the NotBefore date - # this will handle scenarios if ever the NotAfter default is decreased - $newestCertificate = $azStackCertAuthorityCerts | Sort-Object -Property NotBefore -Descending | Select-Object -First 1 - "Using certificate Issuer:{0} Subject:{1} Thumbprint:{2}" -f $newestCertificate.Issuer, $newestCertificate.Subject, $newestCertificate.Thumbprint | Trace-Output + $CimSession = New-SdnCimSession @sessionParams + $cimParams.Add('CimSession', $CimSession) + } - # locate the AzureStackCertificationAuthority certificate within the root store - $certificate = Get-SdnCertificate -Path "Cert:\LocalMachine\Root" -Subject $newestCertificate.Issuer - if ($certificate) { - $certificate = $certificate | Sort-Object -Property NotBefore -Descending | Select-Object -First 1 - } - } - } + try { + $filter = $null + if ($Name) { + $escapedName = $Name -replace "'", "''" + $filter = "ElementName = '$escapedName'" } - # if we not on Azure Stack HCI system, or we did not locate an AzureStackCertificationAuthority certificate - # we will generate a new self-signed certificate that will be used - if ($null -ieq $certificate) { - $certificate = New-SdnSelfSignedCertificate -Subject $subjectName -NotAfter $NotAfter + $switchParams = @{} + $switchParams += $cimParams + if ($filter) { + $switchParams.Add('Filter', $filter) } - # after the certificate has been generated, we want to export the certificate and save the file to directory - # This allows the rest of the function to pick up these files and perform the steps as normal - [System.String]$cerFilePath = "$(Join-Path -Path $CertPath.FullName -ChildPath $subjectName.ToString().ToLower().Replace('.','_').Replace("=",'_').Trim()).cer" - "Exporting certificate to {0}" -f $cerFilePath | Trace-Output - $exportedCertificate = Export-Certificate -Cert $certificate -FilePath $cerFilePath -Type CERT + $switches = Get-CimInstance @switchParams -ClassName 'Msvm_VirtualEthernetSwitch' + $results = [System.Collections.ArrayList]::new() + + foreach ($sw in $switches) { + $switchObject = [PSCustomObject]@{ + Name = $sw.ElementName + SwitchId = $sw.Name + IOVPreferred = $sw.IOVPreferred + Notes = $sw.Notes + MaxIOVOffloads = $sw.MaxIOVOffloads + InstallDate = $sw.InstallDate + } - # distribute the certificate to the Network Controller(s) in the fabric to be installed in trusted root store - if ($FabricDetails) { - "Distributing certificate to SDN Fabric" | Trace-Output - Copy-CertificateToFabric -CertFile $exportedCertificate.FullName -FabricDetails $FabricDetails -ServerNodeCert -Credential $Credential + [void]$results.Add($switchObject) } - return [PSCustomObject]@{ - Certificate = $certificate - FileInfo = $exportedCertificate - } + return ($results | Sort-Object -Property Name) } catch { $_ | Trace-Exception @@ -2765,124 +2756,1434 @@ function New-SdnServerCertificate { } } -function Set-SdnVMNetworkAdapterPortProfile { +function Get-SdnVMNetworkAdapterCim { <# .SYNOPSIS - Configures the port profile applied to the virtual machine network interfaces. + 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. + Specifies the name of the virtual machine whose network adapters are to be retrieved. .PARAMETER MacAddress - Specifies the MAC address of the VM network adapter. - .PARAMETER ProfileId - The InstanceID of the Network Interface taken from Network Controller. If omitted, defaults to an empty GUID to enable network connectivity for non-NC managed VMs. - .PARAMETER ProfileData - 1 = VfpEnabled, 2 = VfpDisabled, 6 = VfpEnabledDHCP. If omitted, defaults to 1. - .PARAMETER HostVmNic - Indicates if NIC is a host NIC. If omitted, defaults to false. - .PARAMETER HyperVHost - Type the NetBIOS name, an IP address, or a fully qualified domain name of the computer that is hosting the virtual machine. + 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 - Set-SdnVMNetworkAdapterPortProfile -VMName 'TestVM01' -MacAddress 001DD826100E -ProfileId -ProfileData 1 + PS> Get-SdnVMNetworkAdapterCim .EXAMPLE - Set-SdnVMNetworkAdapterPortProfile -VMName 'TestVM01' -MacAddress 001DD826100E -ProfileData 2 + PS> Get-SdnVMNetworkAdapterCim -VMName 'VM01' + .EXAMPLE + PS> Get-SdnVMNetworkAdapterCim -All + .EXAMPLE + PS> Get-SdnVMNetworkAdapterCim -ComputerName 'Server01','Server02' -Credential (Get-Credential) #> - [CmdletBinding(DefaultParameterSetName = 'Local')] + [CmdletBinding()] param ( - [Parameter(Mandatory = $true, ParameterSetName = 'Local')] - [Parameter(Mandatory = $true, ParameterSetName = 'Remote')] - [System.String]$VMName, - - [Parameter(Mandatory = $true, ParameterSetName = 'Local')] - [Parameter(Mandatory = $true, ParameterSetName = 'Remote')] - [System.String]$MacAddress, + [Parameter(Mandatory = $false)] + [string]$VMName, - [Parameter(Mandatory = $false, ParameterSetName = 'Local')] - [Parameter(Mandatory = $false, ParameterSetName = 'Remote')] - [System.Guid]$ProfileId = [System.Guid]::Empty, + [Parameter(Mandatory = $false)] + [string]$MacAddress, - [Parameter(Mandatory = $false, ParameterSetName = 'Local')] - [Parameter(Mandatory = $false, ParameterSetName = 'Remote')] - [Int]$ProfileData = 1, + [Parameter(Mandatory = $false)] + [switch]$All, - [Parameter(Mandatory = $false, ParameterSetName = 'Local')] - [Parameter(Mandatory = $false, ParameterSetName = 'Remote')] - [switch]$HostVmNic, + [Parameter(Mandatory = $false)] + [switch]$ManagementOS, - [Parameter(Mandatory = $false, ParameterSetName = 'Remote')] - [System.String]$HyperVHost, + [Parameter(Mandatory = $false)] + [System.String[]]$ComputerName, - [Parameter(Mandatory = $false, ParameterSetName = 'Remote')] + [Parameter(Mandatory = $false)] [System.Management.Automation.PSCredential] [System.Management.Automation.Credential()] $Credential = [System.Management.Automation.PSCredential]::Empty ) - function Set-VMNetworkAdapterPortProfile { - [CmdletBinding()] - param ( - [Parameter(Mandatory = $true, Position = 0)] - [System.String]$VMName, - - [Parameter(Mandatory = $true, Position = 1)] - [System.String]$MacAddress, - - [Parameter(Mandatory = $true, Position = 2)] - [System.Guid]$ProfileId, + $cimNamespace = 'root/virtualization/v2' + $cimParams = @{ + Namespace = $cimNamespace + ErrorAction = 'Stop' + } - [Parameter(Mandatory = $false, Position = 3)] - [System.Int16]$ProfileData = 1, + if ($ComputerName) { + $sessionParams = @{ + ComputerName = $ComputerName + } + if ($Credential -ne [System.Management.Automation.PSCredential]::Empty) { + $sessionParams.Add('Credential', $Credential) + } - [Parameter(Mandatory = $false, Position = 4)] - [switch]$HostVmNic - ) + $cimSession = New-SdnCimSession @sessionParams + $cimParams.Add('CimSession', $cimSession) + } - if ($null -eq (Get-Module -Name Hyper-V)) { - Import-Module -Name Hyper-V -Force -ErrorAction Stop - } + try { + $results = [System.Collections.ArrayList]::new() - [System.Guid]$portProfileFeatureId = "9940cd46-8b06-43bb-b9d5-93d50381fd56" - [System.Guid]$vendorId = "1FA41B39-B444-4E43-B35A-E1F7985FD548" - $vmAdapterParams = @{ - VMName = $VMName - MacAddress = $MacAddress + # 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']) } - if ($HostVmNic) { - $vmAdapterParams.Add('ManagementOS', $true) + $switches = Get-SdnVMSwitchCim @switchCimParams + $switchLookup = @{} + foreach ($sw in $switches) { + $switchLookup[$sw.SwitchId] = $sw.Name } - $vmNic = Get-SdnVmNetworkAdapter @vmAdapterParams - if ($null -eq $vmNic) { - throw New-Object System.ArgumentException("Unable to locate VM $VMName with MacAddress $MacAddress") + # 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 + } + } } - $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 + 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 + } + + # 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 + } + } - $currentProfile = Get-VMSwitchExtensionPortFeature -FeatureId $portProfileFeatureId -VMNetworkAdapter $vmNic - if ($null -eq $currentProfile) { - Add-VMSwitchExtensionPortFeature -VMSwitchExtensionFeature $portProfileDefaultSetting -VMNetworkAdapter $vmNic + [void]$results.Add($adapterObject) + } } - 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 (-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 + $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 + 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-SdnCimAssociatedInstance -InputObject $vmCim -ResultClassName 'Msvm_VirtualSystemSettingData' @cimParams | + Where-Object { $_.VirtualSystemType -eq 'Microsoft:Hyper-V:System:Realized' } + if ($vmSettingData) { + $vmPath = $vmSettingData.CimSystemProperties.CimInstance + $adapters = Get-SdnCimAssociatedInstance -InputObject $vmSettingData -ResultClassName 'Msvm_SyntheticEthernetPortSettingData' @cimParams + } + } + + # Also get emulated adapters (legacy network adapters) + $emulatedAdapters = @() + 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 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) { + $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] + } + } + } + + # 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 + } + } + } - Set-VMSwitchExtensionPortFeature -VMSwitchExtensionFeature $currentProfile -VMNetworkAdapter $vmNic + $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) { + $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) { + 2 { 'Running' } + 3 { 'Off' } + 6 { 'Saved' } + 9 { 'Paused' } + 32768 { 'Paused' } + 32769 { 'Saved' } + 32770 { 'Starting' } + 32771 { 'Snapshotting' } + 32773 { 'Saving' } + 32774 { 'Stopping' } + 32776 { 'Pausing' } + 32777 { 'Resuming' } + default { 'Unknown' } + } + + # Get network adapters from the bulk lookup + $networkAdapters = if ($adaptersByVmGuid.ContainsKey($vm.Name)) { $adaptersByVmGuid[$vm.Name] } else { @() } + + $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-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 + 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 + Get-SdnVMNetworkAdapterPortProfile -All + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'VM')] + [System.String]$VMName, + + [Parameter(Mandatory = $false, ParameterSetName = 'VM')] + [Parameter(Mandatory = $false, ParameterSetName = 'Management')] + [System.String]$Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'All')] + [Switch]$All, + + [Parameter(Mandatory = $true, ParameterSetName = 'Management')] + [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() + + # Get all ethernet port allocation setting data (represents each port connected to a switch) + $portAllocations = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetPortAllocationSettingData' + + # Get port profile settings which contain the profile data + $profileSettings = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetSwitchPortProfileSettingData' + + # 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 + $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 + } + + # 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 + } + } + } + + if ($matchedPort) { + # Look up profile settings for this port + $portInstancePath = $matchedPort.InstanceID + $profile = $profileLookup[$portInstancePath] + if ($null -eq $profile) { + # Try matching by partial path + foreach ($key in $profileLookup.Keys) { + if ($portInstancePath -and $key -like "*$($matchedPort.InstanceID)*") { + $profile = $profileLookup[$key] + break + } + } + } + + if ($profile) { + if ($profile.ProfileId) { + $object.ProfileId = $profile.ProfileId + } + if ($null -ne $profile.ProfileData) { + $object.ProfileData = $profile.ProfileData + } + } + + # Get the port name from the allocation's Name property (VFP device ID) + if ($matchedPort.ElementName) { + $object.PortName = $matchedPort.ElementName + } + } + + [void]$results.Add($object) + } + + return ($results | Sort-Object -Property Name) + } + catch { + $_ | Trace-Exception + $_ | Write-Error + } +} + +function New-SdnServerCertificate { + <# + .SYNOPSIS + Generate new self-signed certificate to be used by the Hyper-V host and distributes to the Network Controller(s) within the environment. + .PARAMETER NotAfter + Specifies the date and time, as a DateTime object, that the certificate expires. To obtain a DateTime object, use the Get-Date cmdlet. The default value for this parameter is one year after the certificate was created. + .PARAMETER Path + Specifies the file path location where a .cer file is exported automatically. + .PARAMETER FabricDetails + The EnvironmentInfo derived from Get-SdnInfrastructureInfo. + .PARAMETER Credential + Specifies a user account that has permission to perform this action. The default is the current user + .EXAMPLE + New-SdnServerCertificate -NotAfter (Get-Date).AddYears(1) -FabricDetails $Global:SdnDiagnostics.EnvironmentInfo + #> + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [datetime]$NotAfter = (Get-Date).AddYears(3), + + [Parameter(Mandatory = $false)] + [System.String]$Path = "$(Get-WorkingDirectory)\ServerCert_{0}" -f (Get-FormattedDateTimeUTC), + + [Parameter(Mandatory = $false)] + [SdnFabricInfrastructure]$FabricDetails, + + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential = [System.Management.Automation.PSCredential]::Empty + ) + + Confirm-IsServer # ensure that the module is running on a Server + Confirm-IsAdmin # ensure that the module is running as local administrator + + try { + if (-NOT (Test-Path -Path $Path -PathType Container)) { + "Creating directory {0}" -f $Path | Trace-Output + $CertPath = New-Item -Path $Path -ItemType Directory -Force + } + else { + $CertPath = Get-Item -Path $Path + } + + $serverCert = Get-ItemPropertyValue -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\NcHostAgent\Parameters' -Name 'HostAgentCertificateCName' -ErrorAction Stop + $subjectName = "CN={0}" -f $serverCert + + # check if there is a certificate present for Azure Stack Certification Authority for Azure Local systems + # if so, we will not generate a new certificate as they should be leveraging the certificate from the Azure Stack CA + if ($Global:SdnDiagnostics.Config.Mode -ieq 'AzureStackHCI') { + $azStackHCICertificates = Get-SdnServerCertificate -NetworkControllerOid -ErrorAction Ignore + if ($azStackHCICertificates) { + $azStackCertAuthorityCerts = $azStackHCICertificates | Where-Object { $_.Issuer -ieq 'CN=AzureStackCertificationAuthority' } + if ($azStackCertAuthorityCerts) { + # pick the most recent certificate based on the NotBefore date + # this will handle scenarios if ever the NotAfter default is decreased + $newestCertificate = $azStackCertAuthorityCerts | Sort-Object -Property NotBefore -Descending | Select-Object -First 1 + "Using certificate Issuer:{0} Subject:{1} Thumbprint:{2}" -f $newestCertificate.Issuer, $newestCertificate.Subject, $newestCertificate.Thumbprint | Trace-Output + + # locate the AzureStackCertificationAuthority certificate within the root store + $certificate = Get-SdnCertificate -Path "Cert:\LocalMachine\Root" -Subject $newestCertificate.Issuer + if ($certificate) { + $certificate = $certificate | Sort-Object -Property NotBefore -Descending | Select-Object -First 1 + } + } + } + } + + # if we not on Azure Stack HCI system, or we did not locate an AzureStackCertificationAuthority certificate + # we will generate a new self-signed certificate that will be used + if ($null -ieq $certificate) { + $certificate = New-SdnSelfSignedCertificate -Subject $subjectName -NotAfter $NotAfter + } + + # after the certificate has been generated, we want to export the certificate and save the file to directory + # This allows the rest of the function to pick up these files and perform the steps as normal + [System.String]$cerFilePath = "$(Join-Path -Path $CertPath.FullName -ChildPath $subjectName.ToString().ToLower().Replace('.','_').Replace("=",'_').Trim()).cer" + "Exporting certificate to {0}" -f $cerFilePath | Trace-Output + $exportedCertificate = Export-Certificate -Cert $certificate -FilePath $cerFilePath -Type CERT + + # distribute the certificate to the Network Controller(s) in the fabric to be installed in trusted root store + if ($FabricDetails) { + "Distributing certificate to SDN Fabric" | Trace-Output + Copy-CertificateToFabric -CertFile $exportedCertificate.FullName -FabricDetails $FabricDetails -ServerNodeCert -Credential $Credential + } + + return [PSCustomObject]@{ + Certificate = $certificate + FileInfo = $exportedCertificate + } + } + catch { + $_ | Trace-Exception + $_ | Write-Error + } +} + +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 + Specifies the MAC address of the VM network adapter. + .PARAMETER ProfileId + The InstanceID of the Network Interface taken from Network Controller. If omitted, defaults to an empty GUID to enable network connectivity for non-NC managed VMs. + .PARAMETER ProfileData + 1 = VfpEnabled, 2 = VfpDisabled, 6 = VfpEnabledDHCP. If omitted, defaults to 1. + .PARAMETER HostVmNic + Indicates if NIC is a host NIC. If omitted, defaults to false. + .PARAMETER HyperVHost + Type the NetBIOS name, an IP address, or a fully qualified domain name of the computer that is hosting the virtual machine. + .PARAMETER Credential + Specifies a user account that has permission to perform this action. The default is the current user. + .EXAMPLE + Set-SdnVMNetworkAdapterPortProfile -VMName 'TestVM01' -MacAddress 001DD826100E -ProfileId -ProfileData 1 + .EXAMPLE + Set-SdnVMNetworkAdapterPortProfile -VMName 'TestVM01' -MacAddress 001DD826100E -ProfileData 2 + #> + + [CmdletBinding(DefaultParameterSetName = 'Local')] + param ( + [Parameter(Mandatory = $true, ParameterSetName = 'Local')] + [Parameter(Mandatory = $true, ParameterSetName = 'Remote')] + [System.String]$VMName, + + [Parameter(Mandatory = $true, ParameterSetName = 'Local')] + [Parameter(Mandatory = $true, ParameterSetName = 'Remote')] + [System.String]$MacAddress, + + [Parameter(Mandatory = $false, ParameterSetName = 'Local')] + [Parameter(Mandatory = $false, ParameterSetName = 'Remote')] + [System.Guid]$ProfileId = [System.Guid]::Empty, + + [Parameter(Mandatory = $false, ParameterSetName = 'Local')] + [Parameter(Mandatory = $false, ParameterSetName = 'Remote')] + [Int]$ProfileData = 1, + + [Parameter(Mandatory = $false, ParameterSetName = 'Local')] + [Parameter(Mandatory = $false, ParameterSetName = 'Remote')] + [switch]$HostVmNic, + + [Parameter(Mandatory = $false, ParameterSetName = 'Remote')] + [System.String]$HyperVHost, + + [Parameter(Mandatory = $false, ParameterSetName = 'Remote')] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential = [System.Management.Automation.PSCredential]::Empty + ) + + function Set-VMNetworkAdapterPortProfile { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, Position = 0)] + [System.String]$VMName, + + [Parameter(Mandatory = $true, Position = 1)] + [System.String]$MacAddress, + + [Parameter(Mandatory = $true, Position = 2)] + [System.Guid]$ProfileId, + + [Parameter(Mandatory = $false, Position = 3)] + [System.Int16]$ProfileData = 1, + + [Parameter(Mandatory = $false, Position = 4)] + [switch]$HostVmNic + ) + + $cimNamespace = 'root/virtualization/v2' + $cimParams = @{ + Namespace = $cimNamespace + ErrorAction = 'Stop' + } + + [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) { + $adapterParams = @{ ManagementOS = $true } + } + + $vmNic = Get-SdnVMNetworkAdapterCim @adapterParams + if ($HostVmNic -and $vmNic) { + $vmNic = $vmNic | Where-Object { (Format-SdnMacAddress -MacAddress $_.MacAddress) -eq $formattedMac } + } + + if ($null -eq $vmNic) { + throw New-Object System.ArgumentException("Unable to locate VM $VMName with MacAddress $MacAddress") + } + + # 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") + } + + # Find existing profile settings for this port + $profileSettings = Get-CimInstance @cimParams -ClassName 'Msvm_EthernetSwitchPortProfileSettingData' + $existingProfile = $null + $portInstancePath = $matchedPort.InstanceID + foreach ($profile in $profileSettings) { + $profilePortPath = $profile.InstanceID -replace '\\[^\\]+\\[^\\]+$', '' + if ($profilePortPath -eq $portInstancePath) { + $existingProfile = $profile + break + } + } + + if ($existingProfile) { + "Current Settings: ProfileId [{0}] ProfileData [{1}]" -f $existingProfile.ProfileId, $existingProfile.ProfileData | Trace-Output + + # 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 + # 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 = @{ @@ -3572,7 +4873,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) @@ -3588,7 +4889,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) { @@ -3641,7 +4942,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/src/modules/SdnDiag.Utilities.psm1 b/src/modules/SdnDiag.Utilities.psm1 index ccd93c17..58a16368 100644 --- a/src/modules/SdnDiag.Utilities.psm1 +++ b/src/modules/SdnDiag.Utilities.psm1 @@ -1829,6 +1829,127 @@ 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 + $_ | Write-Error + } + } + } + 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 { + 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 + } + } + } + catch { + $_ | Trace-Exception + $_ | Write-Error + } +} + function New-PSRemotingSession { [CmdletBinding()] param ( diff --git a/tests/offline/Server.Tests.ps1 b/tests/offline/Server.Tests.ps1 index 35030c0f..f8cc3e39 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 } } @@ -427,3 +427,527 @@ 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) } + 'Msvm_VirtualSystemSettingData' { return @($Global:PesterOfflineTests.CimMockData.VmSettingData) } + default { return @() } + } + } + Mock Get-SdnCimAssociatedInstance { return $null } + 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) } + 'Msvm_VirtualSystemSettingData' { return @($Global:PesterOfflineTests.CimMockData.VmSettingData) } + default { return @() } + } + } + Mock Get-SdnCimAssociatedInstance { + return $null + } + 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 @() } + 'Msvm_VirtualSystemSettingData' { return @($Global:PesterOfflineTests.CimMockData.VmSettingData) } + 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' + ElementName = 'port001' + } + ) + } + 'Msvm_EthernetSwitchPortProfileSettingData' { + return @( + [PSCustomObject]@{ + 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}' + } + ) + } + 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 } + } + } +} + +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 = 'Microsoft:VMGUID\PortGUID' + }) + } + 'Msvm_EthernetSwitchPortVlanSettingData' { + return @([PSCustomObject]@{ + InstanceID = 'Microsoft:VMGUID\PortGUID\C\VlanGUID' + 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 = 'Microsoft:VMGUID\PortGUID' }) + } + 'Msvm_EthernetSwitchPortIsolationSettingData' { + return @([PSCustomObject]@{ + InstanceID = 'Microsoft:VMGUID\PortGUID\C\IsoGUID' + 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 = 'Microsoft:VMGUID\PortGUID' }) + } + 'Msvm_EthernetSwitchPortExtendedAclSettingData' { + return @( + [PSCustomObject]@{ + InstanceID = 'Microsoft:VMGUID\PortGUID\C\AclGUID' + 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 = 'Microsoft:VMGUID\PortGUID' }) + } + 'Msvm_EthernetSwitchPortRoutingDomainSettingData' { + return @( + [PSCustomObject]@{ + InstanceID = 'Microsoft:VMGUID\PortGUID\C\RdGUID' + 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 + } + } +} 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 + } + } + } +}