Convert-LBFO2SET.psm1

<#

Changelog:

29 Aug 2022
   - Added Windows Server 2022+ support.
   - Fixed an SCVMM compatibilty issue.
   -

#>



Function Convert-LBFO2Set
{
    <#
        .SYNOPSIS
            Virtual Switches attached to an LBFO team has been deprecated by Microsoft. Use this tool, to
            migrate your team to the alternative teaming solution, Switch Embedded Teaming (SET).
    
        .DESCRIPTION
            This script will allow you to migrate a LBFO Team into a SET team. It will also migrate a vSwitch (if added to the LBFO Team)
            To a new vSwitch on SET including the vNICs. This enables you to migrate a host with active virtual machines.
    
        .PARAMETER LBFOTeam
            The name of the LBFO Team to be migrated
    
        .PARAMETER SETTeam
            The name of the SET team to be created (the team does not need to already exist)
    
        .PARAMETER AllowOutage
            Use this to allow a migration of a team and vSwitch with only one pNIC. In this case, the migration will incur an outage
            for any virtual NICs connected to the team because the underlying pNIC can only be connected to one team at a time
    
        .PARAMETER EnableBestPractices
            Use this to set Microsoft recommended best practices for the team and/or Virtual Switch. If this switch is omitted, the
            existing settings from the LBFO team will be configured on SET
    
        .EXAMPLE
            Convert-LBFO2Set -LBFOTeam NameofLBFOTeam -SETTeam NewSETTeamName
    
        .EXAMPLE
            Convert-LBFO2Set TODO
    
        .NOTES
            Author: Microsoft Edge OS Networking Team and Microsoft CSS
    
            Please file issues on GitHub @ GitHub.com/Microsoft/Convert-LBFO2SET
    
        .LINK
            More projects : https://github.com/topics/msftnet
            Windows Networking Blog : https://blogs.technet.microsoft.com/networking/
    #>


    [CmdletBinding()]
    param (
        [parameter(Mandatory = $true)]
        [String]$LBFOTeam,
        [parameter(Mandatory = $true)]
        [String]$SETTeam,
        [parameter(Mandatory = $False)]
        [Switch]$AllowOutage = $false,
        [parameter(Mandatory = $False)]
        [Switch]$EnableBestPractices = $false
    )
    
    Write-Verbose "Collecting data and validating configuration."
    # check whether $LBFOTeam is the vSwitch or the LBFO team name bound to a vSwitch
    # if there is an LBFO team with the name we simply use that
    $isLBFOTeam = Get-NetLbfoTeam -Name $LBFOTeam -ErrorAction SilentlyContinue
    if (-NOT $isLBFOTeam)
    {
        # check to see whether this is a vSwitch
        $isvSwitch = Get-VMSwitch $LBFOTeam -ErrorAction SilentlyContinue
        if ($isvSwitch)
        {
            Write-Verbose "LBFOTeam is a vSwitch. Verifying that an LBFO team is attached."
            ## a vSwitch was found. Now make sure there is an LBFO team attached.
            # get the vSwitch adapter(s) based on the InterfaceDescription contained in the vSwitch object
            $tmpAdapter = Get-NetAdapter | Where-Object InterfaceDescription -in $isvSwitch.NetAdapterInterfaceDescriptions
            # compare to list of LBFO team adapters
            $tmpTeam = Get-NetLbfoTeam | Where-Object { $_.Name -in $tmpAdapter.Name -or $_.Name -eq $tmpAdapter.Name }
            if ($tmpTeam)
            {
                # we found the LBFO team attached to the vSwitch! Set that to $LBFOTeam. We'll rediscover the vSwitch later.
                $LBFOTeam = $tmpTeam.Name
            }
            else
            {
                Throw "An LBFO team associated with $LBFOTeam could not be detected."
            }
        }
        else
        {
            Throw "Failed to find an LBFO team or vSwitch named $LBFOTeam`."
        }
        Remove-Variable isLBFOTeam, isvSwitch, tmpAdapter, tmpTeam -ErrorAction SilentlyContinue
    }
    else
    {
        Remove-Variable isLBFOTeam -ErrorAction SilentlyContinue
    }
    # get the path to where the module is stored
    $here = Split-Path -Parent (Get-Module -Name Convert-LBFO2SET).Path
    if (-NOT $here)
    {
        throw "Could not find the module path."
    }
    # detect the version of Windows
    $osMajVer = [System.Environment]::OSVersion.Version.Major
    $osBldVer = [System.Environment]::OSVersion.Version.Build
    switch ($osMajVer)
    {
        10
        {
            switch ($osBldVer)
            {
                # Windows Server 2016
                14393 {
                    $nicReconnBin = "nicReconnect1.exe"
                }
                # Windows Server 2019+
                { $_ -ge 17763 } {
                    $nicReconnBin = "nicReconnect5.exe"
                }
                default
                {
                    throw "This version of Windows is not yet certified for Convert-LBFO2SET."
                }
            }
        }
        default
        {
            throw "A supported version of Windows was not detected."
        }
    }
    #region Data Collection
    $configData = @{ NetLBFOTeam = Get-NetLbfoTeam -Name $LBFOTeam -ErrorAction SilentlyContinue }
    $ValidationResults = Invoke-Pester -Script "$here\tests\unit\unit.tests.ps1" -Tag PreValidation -PassThru
    $ValidationResults | Select-Object -Property TagFilter, Time, TotalCount, PassedCount, FailedCount, SkippedCount, PendingCount | Format-Table -AutoSize
    If ($ValidationResults.FailedCount -ne 0)
    {
        Write-Warning 'Prerequisite checks have failed.'
        Write-Warning "`n`nPlease note: if the failure was due to LACP: `n`t - We will intentionally NOT convert this type of team as the new team will not be functional until the port-channel on the physical switch has been modified"
        Write-Warning "To continue with an LACP conversion, please break the port-channel on the physical switch and modify the LBFO team to Switch Independent (Set-NetLbfoTeam -TeamingMode SwitchIndependent)"
        throw
    }
    $configData += @{
        NetAdapter = Get-NetAdapter -Name $configData.NetLBFOTeam.TeamNics -ErrorAction SilentlyContinue
        NetAdapterBinding = Get-NetAdapterBinding -Name $configData.NetLBFOTeam.TeamNics -ErrorAction SilentlyContinue
    }
    $configData += @{
        LBFOVMSwitch = Get-VMSwitch -ErrorAction SilentlyContinue | Where-Object NetAdapterInterfaceGuid -eq $configData.NetAdapter.InterfaceGuid
    }
    if ($ConfigData.LBFOVMSwitch)
    {
        $configData += @{
            VMNetworkAdapter = Get-VMNetworkAdapter -All | Where-Object SwitchName -EQ $configData.LBFOVMSwitch.Name -ErrorAction SilentlyContinue
        }
        # Grabbing host vNICs (ManagementOS) attached to the LBFO vSwitch
        $configData += @{ HostvNICs = @(Get-VMNetworkAdapter -ManagementOS -SwitchName $configData.LBFOVMSwitch.Name) }
    }
    #endregion
    # EnableIOV should be $true as a best practice unless Hyper-V QoS is in use. Enabling IOV turns the vSwitch Bandwidth mode to 'None' so no legacy QoS
    Write-Verbose "Bandwidth Reservation Mode: $($ConfigData.LBFOVMSwitch.BandwidthReservationMode)"
    Switch ($ConfigData.LBFOVMSwitch.BandwidthReservationMode)
    {
        { 'Absolute' -or 'Weight' } {
            If ($configData.LBFOVMSwitch.IovEnabled)
            {
                $IovEnabled = $true
            }
            Else
            {
                $IovEnabled = $false
            }
        }
        'None' { $IovEnabled = $true }
        default { $IovEnabled = $false }
    }
    #region Create new SET team
    #TODO: test this logic thuroughly...
    if ($AllowOutage -eq $true -and $configData.NetLBFOTeam.Members.Count -eq 1)
    {
        $NetAdapterNames = $configData.NetLBFOTeam.Members
        # Only one pnIC - Destroy the LBFOTeam
        Remove-NetLbfoTeam -Name $configData.NetLBFOTeam.Name -Confirm:$false
    }
    else
    {
        $NetAdapterNames = $configData.NetLBFOTeam.Members[0]
        Remove-NetLbfoTeamMember -Name $configData.NetLBFOTeam.Members[0] -Team $configData.NetLBFOTeam.Name -Confirm:$False
    }
    Write-Verbose "IOV Enabled: $IovEnabled"
    $SETTeamParams = @{
        Name                  = $SETTeam
        NetAdapterName          = $NetAdapterNames
        EnablePacketDirect    = $false
        EnableEmbeddedTeaming = $true
        AllowManagementOS      = $false
        MinimumBandwidthMode  = $($ConfigData.LBFOVMSwitch.BandwidthReservationMode)
        EnableIov              = $IovEnabled
    }
    Write-verbose "SETTeam Parameters: $($SETTeamParams | Out-String)"
    $vSwitchExists = Get-VMSwitch -Name $SETTeam -ErrorAction SilentlyContinue
    if (-NOT $vSwitchExists)
    {
        New-VMSwitch @SETTeamParams
    }
    else
    {
        $VerbosePreference = 'Continue'
        Write-Verbose "Team named [$SETTeam] exists and will be used"
        $VerbosePreference = 'SilentlyContinue'
    }
    Remove-Variable SETTeamParams -ErrorAction SilentlyContinue
    #endregion
    $vmNICs = ($configData.VMNetworkAdapter | Where-Object VMName -ne $Null)
    $vNICMigrationNeeded = If ($vmNICs) { $true } else { $false }
    # TODO: Add vmNIC and Host vNIC to test cases.
    if ($vNICMigrationNeeded) { Connect-VMNetworkAdapter -VMNetworkAdapter $vmNICs -SwitchName $SETTeam -ErrorAction SilentlyContinue }
    # migrate host vNIC(s)
    Foreach ($HostvNIC in $configData.HostvNics)
    {
        & "$($here)\helpers\$($nicReconnBin)" -r "$($HostvNIC.Name)" "$SETTeam" | Out-Null
    }
    # validation to make sure there are no more vmNICs attached
    $vmMigGood = Get-VMNetworkAdapter -All | Where-Object SwitchName -EQ $configData.LBFOVMSwitch.Name -ErrorAction SilentlyContinue
    if ($vmMigGood)
    {
        throw "Critical vmNIC migration failure. The following virtual NICs were not migrated to the new SET switch:`n$($vmMigGood | ForEach-Object {
                "`n`t$($_.Name) [$(if ($_.VMName) { "$($_.VMName)" }
                    else { "host" })] "
            })"

    }
    #region Fire and Brimstone
    $remainingAdapters = $configData.NetLBFOTeam.Members
    if ($configData.LBFOVMSwitch)
    {
        Remove-VMSwitch -Name $configData.LBFOVMSwitch.Name -Force -ErrorAction SilentlyContinue | Out-Null
    }
    Remove-NetLbfoTeam -Name $configData.NetLBFOTeam.Name -Confirm:$false -ErrorAction SilentlyContinue
    #TODO: May need to check that the switch and / or team actually were removed before moving on
    Add-VMSwitchTeamMember -NetAdapterName $remainingAdapters -VMSwitchName $SETTeam
    Remove-Variable HostvNIC -ErrorAction SilentlyContinue
        <# Temporarily removing till we work through host vNIC migration plan
        foreach ($HostvNIC in ($configData.HostvNICs)) {
            $NewNetAdapterName = $configData.HostvNICs.$($HostvNIC.Keys).HostvNICNetAdapter.Name
    
            Rename-NetAdapter -Name "$NewNetAdapterName-446f776e2057697468204c42464f" -NewName $NewNetAdapterName
        }
        #>

    #endregion
    Write-Verbose "Enable Best Practices Parameter Present: $EnableBestPractices"
    if ($EnableBestPractices)
    {
        $SETInterfaces = (Get-VMSwitchTeam -Name $SETTeam).NetAdapterInterfaceDescription
        $SETAdapters = (Get-NetAdapter | Where-Object InterfaceDescription -in $SETInterfaces).Name
        Foreach ($interface in $SETAdapters)
        {
            Reset-NetAdapterAdvancedProperty -Name $interface -ErrorAction SilentlyContinue `
                                             -DisplayName 'NVGRE Encapsulated Task Offload', 'VXLAN Encapsulated Task Offload', 'IPV4 Checksum Offload',
                                             'NetworkDirect Technology', 'Recv Segment Coalescing (IPv4)', 'Recv Segment Coalescing (IPv6)',
                                             'Maximum number of RSS Processors', 'Maximum Number of RSS Queues', 'RSS Base Processor Number',
                                             'RSS Load Balancing Profile', 'SR-IOV', 'TCP/UDP Checksum Offload (IPv4)', 'TCP/UDP Checksum Offload (IPv6)'
            Set-NetAdapterAdvancedProperty -Name $interface -DisplayName 'Packet Direct' -RegistryValue 0 -ErrorAction SilentlyContinue
            Set-NetAdapterAdvancedProperty -Name $interface -RegistryValue 1 -DisplayName 'Receive Side Scaling', 'Virtual Switch RSS', 'Virtual Machine Queues', 'NetworkDirect Functionality' -ErrorAction SilentlyContinue
        }
        $NodeOSCaption = (Get-CimInstance -ClassName 'Win32_OperatingSystem').Caption
        Switch -Wildcard ($NodeOSCaption)
        {
            '*Windows Server 2016*' {
                $SETSwitchUpdates = @{ DefaultQueueVrssQueueSchedulingMode = 'StaticVRSS' }
                $vmNICUpdates = @{ VrssQueueSchedulingMode = 'StaticVRSS' }
                $HostvNICUpdates = @{ VrssQueueSchedulingMode = 'StaticVRSS' }
            }
            '*Windows Server 2019*' {
                $SETSwitchUpdates = @{
                    EnableSoftwareRsc                    = $true
                    DefaultQueueVrssQueueSchedulingMode = 'Dynamic'
                }
                $vmNICUpdates = @{ VrssQueueSchedulingMode = 'Dynamic' }
                $HostvNICUpdates = @{ VrssQueueSchedulingMode = 'Dynamic' }
            }
        }
        $SETSwitchUpdates += @{
            Name                          = $SETTeam
            DefaultQueueVrssEnabled          = $true
            DefaultQueueVmmqEnabled          = $true
            DefaultQueueVrssMinQueuePairs = 8
            DefaultQueueVrssMaxQueuePairs = 16
        }
        $vmNICUpdates += @{
            VMName              = '*'
            VrssEnabled          = $true
            VmmqEnabled          = $true
            VrssMinQueuePairs = 8
            VrssMaxQueuePairs = 16
        }
        $HostvNICUpdates += @{
            ManagementOS      = $true
            VrssEnabled          = $true
            VmmqEnabled          = $true
            VrssMinQueuePairs = 8
            VrssMaxQueuePairs = 16
        }
        Set-VMSwitch @SETSwitchUpdates
        Set-VMSwitchTeam -Name $SETTeam -LoadBalancingAlgorithm HyperVPort
        Set-VMNetworkAdapter @HostvNICUpdates
        Set-VMNetworkAdapter @vmNICUpdates
        Remove-Variable SETSwitchUpdates, vmNICUpdates, HostvNICUpdates, NodeOSCaption -ErrorAction SilentlyContinue
    }
}