DataCenterBridging.psm1

#region DCB DSC Resources
enum Ensure {
    Absent
    Present
}

[DscResource()]
Class DCBNetQosFlowControl {
    [DscProperty(Mandatory)]
    [Ensure] $Ensure

    [DscProperty(Key)]
    [ValidateRange(0,7)]
    [int] $Priority

    [DscProperty(NotConfigurable)]
    [Boolean] $Enabled

    [DCBNetQosFlowControl] Get() {
        $FlowControlPriority = Get-NetQosFlowControl -Priority $this.Priority

        $this.Enabled = $FlowControlPriority.Enabled
        $this.Priority = $FlowControlPriority.Priority

        return $this
    }

    [bool] Test() {
        $FlowControlPriority = Get-NetQosFlowControl -Priority $this.Priority

        $testState = $false
        if ($this.Ensure -eq [Ensure]::Present) {
            Switch ($FlowControlPriority.Enabled) {
                $true {$testState = $true}
                $false {$testState =  $false}
            }
        }
        elseif ($this.Ensure -eq [Ensure]::Absent) {
            Switch ($FlowControlPriority.Enabled) {
                $true {$testState =  $false}
                $false {$testState =  $true}
            } 
        }

        Return $testState
    }

    [Void] Set() {
        if ($this.Ensure -eq [Ensure]::Present) {
            Write-Verbose "Enabling priority $($this.Priority)"
            Set-NetQosFlowControl -Priority $this.Priority -Enabled $true
            Write-Verbose "Priority $($this.Priority) is now Enabled"
        }
        elseif ($this.Ensure -eq [Ensure]::Absent) {
            Write-Verbose "Enabling priority $($this.Priority)"
            Set-NetQosFlowControl -Priority $this.Priority -Enabled $false
            Write-Verbose "Priority $($this.Priority) is now disabled"
        }
    }
}

[DscResource()]
Class DCBNetAdapterQos {
    [DscProperty(Mandatory)]
    [Ensure] $Ensure

    [DscProperty(Key)]
    [string] $InterfaceName

    [DscProperty(NotConfigurable)]
    [Boolean] $Enabled

    [DCBNetAdapterQos] Get() {
        $NetAdapterQosState = Get-NetAdapterQos -Name $this.InterfaceName

        $this.InterfaceName = $NetAdapterQosState.Name
        $this.Enabled = $NetAdapterQosState.Enabled

        return $this
    }

    [bool]Test() {
        $NetAdapterQosState = Get-NetAdapterQos -Name $this.InterfaceName

        $testState = $false

        if ($this.Ensure -eq [Ensure]::Present) {
            Switch ($NetAdapterQosState.Enabled) {
                $true {$testState = $true}
                $false {$testState =  $false}
            }
        }
        elseif ($this.Ensure -eq [Ensure]::Absent) {
            Switch ($NetAdapterQosState.Enabled) {
                $true {$testState =  $false}
                $false {$testState =  $true}
            } 
        }

        Return $testState
    }

    [Void] Set() {
        if ($this.Ensure -eq [Ensure]::Present) {
            Write-Verbose "Enabling QoS on $($this.InterfaceName)"
            Set-NetAdapterQos -Name $this.InterfaceName -Enabled $true
            Write-Verbose "QoS is now enabled on $($this.InterfaceName)"
        }
        elseif ($this.Ensure -eq [Ensure]::Absent) {
            Write-Verbose "Disabling QoS on $($this.InterfaceName)"
            Set-NetAdapterQos -Name $this.InterfaceName -Enabled $false
            Write-Verbose "QoS is now disabled on $($this.InterfaceName)"
        }
    }
}

[DscResource()]
Class DCBNetQosDcbxSetting {
    [DscProperty(Key)]
    [Ensure] $Ensure

    [DCBNetQosDcbxSetting] Get() {
        $NetQosDcbx = Get-NetQosDcbxSetting

        if ($this.Ensure) {
            $this.Willing = $NetQosDcbx.Willing
        }

        return $this
    }

    [bool]Test() {
        $NetQosDcbx = Get-NetQosDcbxSetting

        $testState = $false

        if ($this.Ensure -eq [Ensure]::Present) {
            Switch ($NetQosDcbx.Willing) {
                $true {$testState = $true}
                $false {$testState =  $false}
            }
        }
        elseif ($this.Ensure -eq [Ensure]::Absent) {
            Switch ($NetQosDcbx.Willing) {
                $true {$testState =  $false}
                $false {$testState =  $true}
            } 
        }

        Return $testState
    }

    [Void] Set() {
        if ($this.Ensure -eq [Ensure]::Present) {
            Write-Verbose "Enabling DCBX Willing bit"
            Write-Verbose "Note: DCBX is not supported on Windows Server 2016 or Windows Server 2019"
            Set-NetQosDcbxSetting -Willing $true
            Write-Verbose "DCBX Willing bit is now enabled"
            Write-Verbose "Note: DCBX is not supported on Windows Server 2016 or Windows Server 2019"
        }
        elseif ($this.Ensure -eq [Ensure]::Absent) {
            Write-Verbose "Disabling DCBX Willing bit"
            Set-NetQosDcbxSetting -Willing $false
            Write-Verbose "DCBX Willing bit is now disabled"
        }
    }
}

[DscResource()]
Class DCBNetQosPolicy {
    [DscProperty(Mandatory)]
    [Ensure] $Ensure

    [DscProperty(Key)]
    [string] $Name

    [DscProperty(Mandatory)]
    [string] $PriorityValue8021Action

    [DscProperty()]
    [ValidateSet('Default', 'SMB', 'Cluster', 'LiveMigration')]
    [string] $Template

    [DscProperty()]
    [string] $NetDirectPortMatchCondition = 0

    [DCBNetQosPolicy] Get() {
        $NetQosPolicy = Get-NetQosPolicy -Name $this.Name -ErrorAction SilentlyContinue

        $this.Name = $NetQosPolicy.Name
        $this.PriorityValue8021Action = $NetQosPolicy.PriorityValue8021Action
        
        if ($NetQosPolicy.Template -ne 'None') { $this.Template = $NetQosPolicy.Template }
        
        if ($NetQosPolicy.NetDirectPortMatchCondition -ne '0') { 
            $this.NetDirectPortMatchCondition = $NetQosPolicy.NetDirectPortMatchCondition
        }

        return $this
    }

    [bool] Test() {
        $NetQosPolicy = Get-NetQosPolicy -Name $this.Name -ErrorAction SilentlyContinue

        $testState = $false

        If ($NetQosPolicy) {
            Switch ($this.Ensure) {
                'Present' {
                    $teststate = $true

                    If ($NetQosPolicy.PriorityValue8021Action -ne $this.PriorityValue8021Action) { $teststate = $false }

                    If ($this.Template) {
                        If ($NetQosPolicy.Template -ne $this.Template) { $teststate = $false }
                    }
                    
                    If ($this.NetDirectPortMatchCondition) {
                        If ($NetQosPolicy.NetDirectPortMatchCondition -ne $this.NetDirectPortMatchCondition) { $teststate = $false }
                    }
                }

                'Absent' { $teststate = $false }
            }
        } else {
            Switch ($this.Ensure) {
                'Present' { $teststate = $false }
                'Absent' { $teststate = $true }
            }
        }

        Return $testState
    }

    [Void] Set() {
        $NetQosPolicy = Get-NetQosPolicy -Name $this.Name -ErrorAction SilentlyContinue

        If ($NetQosPolicy) {
            Switch ($this.Ensure) {
                'Present' {
                    If ($this.PriorityValue8021Action) {
                        If ($NetQosPolicy.PriorityValue8021Action -ne $this.PriorityValue8021Action) {
                            Write-Verbose "Correcting the priority value of NetQosPolicy $($this.Name) to $($this.PriorityValue8021Action)"
                            Set-NetQosPolicy -Name $this.Name -PriorityValue8021Action $this.PriorityValue8021Action
                            Write-Verbose "Corrected the priority value of NetQosPolicy $($this.Name) to $($this.PriorityValue8021Action)"
                        }
                    }
                    
                    if ($this.NetDirectPortMatchCondition) {
                        If ($NetQosPolicy.NetDirectPortMatchCondition -ne $this.NetDirectPortMatchCondition) {
                            Write-Verbose "Correcting the NetDirectPortMatchCondition value of NetQosPolicy $($this.Name) to $($this.NetDirectPortMatchCondition)"
                            Set-NetQosPolicy -Name $this.Name -NetDirectPortMatchCondition $this.NetDirectPortMatchCondition
                            Write-Verbose "Corrected the NetDirectPortMatchCondition value of NetQosPolicy $($this.Name) to $($this.NetDirectPortMatchCondition)"
                        }
                    }

                    If ($this.Template) {
                        If ($NetQosPolicy.Template -ne $this.Template) {
                            Write-Verbose "Correcting the Template value of NetQosPolicy $($this.Name) to $($this.Template)"
                            $templateParam = @{ $this.Template = $true }
                            Set-NetQosPolicy -Name $this.Name @templateParam
                            Write-Verbose "Corrected the Template value of NetQosPolicy $($this.Name) to $($this.Template)"
                        }
                    }
                }

                'Absent' { 
                    Write-Verbose "Removing NetQosPolicy $($this.Name)"
                    Remove-NetQosPolicy -Name $this.Name
                    Write-Verbose "NetQosPolicy $($this.Name) has been removed"
                }
            }
        } else {
            if ($this.Ensure -eq [Ensure]::Present) {
                if ($this.NetDirectPortMatchCondition -ne 0) {
                    Write-Verbose "Creating NetQosPolicy $($this.Name)"
                    New-NetQosPolicy -Name $this.Name -PriorityValue8021Action $this.PriorityValue8021Action -NetDirectPortMatchCondition $this.NetDirectPortMatchCondition
                    Write-Verbose "NetQosPolicy $($this.Name) has been created"
                }
                elseif ($this.Template -ne 'None') {
                    Write-Verbose "Creating NetQosPolicy $($this.Name)"
                    $templateParam = @{ $this.Template = $true }
                    New-NetQosPolicy -Name $this.Name -PriorityValue8021Action $this.PriorityValue8021Action @templateParam
                    Write-Verbose "NetQosPolicy $($this.Name) has been created"
                }
                else { Write-Verbose 'Catastrophic Failure' }
            } 
        }
    }
}

[DscResource()]
Class DCBNetQosTrafficClass {
    [DscProperty(Mandatory)]
    [Ensure] $Ensure

    [DscProperty(Key)]
    [string] $Name

    [DscProperty(Mandatory)]
    [string] $Priority

    [DscProperty(Mandatory)]
    [ValidateRange(1,99)]
    [string] $BandwidthPercentage

    [DscProperty(Mandatory)]
    [ValidateSet('ETS','Strict')]
    [string] $Algorithm = 'ETS'

    [DCBNetQosTrafficClass] Get() {
        $NetQosTrafficClass = Get-NetQosTrafficClass -Name $this.Name -ErrorAction SilentlyContinue

        $this.Name = $NetQosTrafficClass.Name
        $this.Priority = $NetQosTrafficClass.Priority
        $this.BandwidthPercentage = $NetQosTrafficClass.BandwidthPercentage
        $this.Algorithm = $NetQosTrafficClass.Algorithm

        return $this
    }

    [bool] Test() {
        $NetQosTrafficClass = Get-NetQosTrafficClass -Name $this.Name -ErrorAction SilentlyContinue

        $testState = $false

        If ($NetQosTrafficClass) {
            Switch ($this.Ensure.ToString()) {
                'Present' {
                    $teststate = $true
                    If ($NetQosTrafficClass.Priority -ne $this.Priority) { $teststate = $false }
                    If ($NetQosTrafficClass.BandwidthPercentage -ne $this.BandwidthPercentage) { $teststate = $false }
                    If ($NetQosTrafficClass.Algorithm -ne $this.Algorithm) { $teststate = $false }
                }

                'Absent' { $teststate = $false }
            }
        } else {
            Switch ($this.Ensure) {
                'Present' { $teststate = $false }
                'Absent' { $teststate = $true }
            }
        }

        Return $testState
    }

    [Void] Set() {
        $NetQosTrafficClass = Get-NetQosTrafficClass -Name $this.Name -ErrorAction SilentlyContinue

        If ($NetQosTrafficClass) {
            Switch ($this.Ensure) {
                'Present' {
                    If ($NetQosTrafficClass.Priority -ne $this.Priority) {
                        Write-Verbose "Correcting the priority value of NetQosTrafficClass $($this.Name) to $($this.Priority)"
                        Set-NetQosTrafficClass -Name $this.Name -Priority $this.Priority
                        Write-Verbose "Corrected the priority value of NetQosTrafficClass $($this.Name) to $($this.Priority)"
                    }

                    If ($NetQosTrafficClass.BandwidthPercentage -ne $this.BandwidthPercentage) {
                        Write-Verbose "Correcting the BandwidthPercentage value of NetQosTrafficClass $($this.Name) to $($this.BandwidthPercentage)"
                        Set-NetQosTrafficClass -Name $this.Name -BandwidthPercentage $this.BandwidthPercentage
                        Write-Verbose "Corrected the BandwidthPercentage value of NetQosTrafficClass $($this.Name) to $($this.BandwidthPercentage)"
                    }

                    If ($NetQosTrafficClass.Algorithm -ne $this.Algorithm) {
                        Write-Verbose "Correcting the Algorithm value of NetQosTrafficClass $($this.Name) to $($this.Algorithm)"
                        Set-NetQosTrafficClass -Name $this.Name -Algorithm $this.Algorithm
                        Write-Verbose "Corrected the Template value of NetQosTrafficClass $($this.Name) to $($this.Algorithm)"
                    }
                }

                'Absent' { 
                    Write-Verbose "Removing NetQosTrafficClass $($this.Name)"
                    Remove-NetQosTrafficClass -Name $this.Name
                    Write-Verbose "NetQosTrafficClass $($this.Name) has been removed"
                }
            }
        } else {
            if ($this.Ensure -eq [Ensure]::Present) {
                Write-Verbose "Creating NetQosTrafficClass $($this.Name)"
                New-NetQosTrafficClass -Name $this.Name -Priority $this.Priority -BandwidthPercentage $this.BandwidthPercentage -Algorithm $this.Algorithm
                Write-Verbose "NetQosTrafficClass $($this.Name) has been created"
            } 
        }
    }
}
#endregion DCB DSC Resources

#region FabricInfo
#region Helper Functions (Not Exported)
Function Get-Interfaces {
    param (
        [Parameter(Mandatory=$false)]
        [String[]] $InterfaceNames,

        [Parameter(Mandatory=$false)]
        [String] $SwitchName
    )

    If ($SwitchName) {
        $VMSwitchTeam = Get-VMSwitchTeam -Name $SwitchName
        $Interfaces   = Get-NetAdapter -InterfaceDescription $VMSwitchTeam.NetAdapterInterfaceDescription
    }
    elseif ($InterfaceNames) { $Interfaces = Get-NetAdapter -Name $InterfaceNames }

    Return $Interfaces
}
#region LLDP
Function Invoke-BitShift {
    param (
        [Parameter(Mandatory,Position=0)]
        [int] $x ,

        [Parameter(ParameterSetName='Left')]
        [ValidateRange(0,[int]::MaxValue)]
        [int] $Left ,

        [Parameter(ParameterSetName='Right')]
        [ValidateRange(0,[int]::MaxValue)]
        [int] $Right
    )

    $shift = If($PSCmdlet.ParameterSetName -eq 'Left') { $Left }
            Else { -$Right }

    Return [math]::Floor($x * [math]::Pow(2,$shift))
}

Function Get-LLDPEvents {
    param ($RemainingIndexes)

    $EventPerInterface = @()
    :enoughEvents while ($remainingIndexes -ne $Null) {
        $CuratedEvents = Get-WinEvent -LogName Microsoft-Windows-LinkLayerDiscoveryProtocol/Diagnostic -Oldest -ErrorAction SilentlyContinue |
            Where-Object { $_.ID -eq 10041 -and $_.TimeCreated -ge (Get-Date).AddMinutes(-5)} | Sort-Object TimeCreated -Descending

        $CuratedEvents | ForEach-Object {
            $thisEvent = $_

            if ($remainingIndexes -contains $thisEvent.Properties[0].Value) {
                $remainingIndexes = $remainingIndexes | Where-Object { $_ -ne $thisEvent.Properties[0].Value }
                $EventPerInterface += $thisEvent
            }

            # Note: We only want one event per index, so we'll break here if we received enough events
            if ($remainingIndexes -eq $null) {break enoughEvents}
        }

        break enoughEvents
    }

    if ($RemainingIndexes -ne $Null) { $global:IndexesMissingEvents = $RemainingIndexes }

    return $EventPerInterface
}

Function Parse-LLDPPacket {
    param ($Events)

    $Table = @()

    $tlv = @{
        ChassisId            = 1
        PortId               = 2
        TimeToLive           = 3
        PortDescription      = 4
        SystemName           = 5
        OrganizationSpecific = 127
    }

    [Flags()] enum PFC_lower {
        Priority0 = 1
        Priority1 = 2
        Priority2 = 4
        Priority3 = 8
    }

    [Flags()] enum PFC_higher {
        Priority4 = 1
        Priority5 = 2
        Priority6 = 4
        Priority7 = 8
    }

    $Events | ForEach-Object {
        $thisEvent = $_
        $offset = 14
        $VLANID = @()
        $bytes = $thisEvent.Properties[3].Value

        $Destination = "{0:X2}:{1:X2}:{2:X2}:{3:X2}:{4:X2}:{5:X2}" -f $bytes[0..5]
        $SourceMac   = "{0:X2}:{1:X2}:{2:X2}:{3:X2}:{4:X2}:{5:X2}" -f $bytes[6..11]
        $EtherType   = "0x$([BitConverter]::ToString($bytes[12]) + [BitConverter]::ToString($bytes[13]))"

        While ($bytes[$offset] -ne 0) {
            $type   = $bytes[$Offset] -shr 1
            $Length = $Length = (Invoke-BitShift ($bytes[$offset] -band 1) -left 8) -bor $bytes[$offset + 1]

            Switch ($type) {
                $tlv.ChassisID {
                    Switch ($bytes[$offset + 2]) {
                        # Mac Address SubType
                        4 { $ChassisID = "{0:X2}:{1:X2}:{2:X2}:{3:X2}:{4:X2}:{5:X2}" -f $bytes[($offset + 3)..($offset + 8)] }
                    }
                }

                $tlv.PortDescription { $PortDescription = ([System.Text.Encoding]::ASCII.GetString($bytes[$Offset..($Offset + $Length + 1)])).Trim() }
                $tlv.SystemName      { $SystemName = ([System.Text.Encoding]::ASCII.GetString($bytes[$Offset..($Offset + $Length + 1)])).Trim()      }

                $tlv.OrganizationSpecific {
                    $OUI = [System.BitConverter]::ToString($bytes[($Offset+2)..($Offset + 4)]).Replace('-', ':')

                    Switch ($bytes[$offset + 5]) {
                        # Additional Subtypes - https://wiki.wireshark.org/LinkLayerDiscoveryProtocol#:~:text=All%20Organizationally%20Specific%20TLVs%20start%20with%20an%20LLDP,followed%20by%20a%201%20octet%20organizationally%20defined%20subtype
                        {$_ -eq '1' -and $OUI -eq '00:80:C2'} {
                            $NativeVLAN = (Invoke-BitShift $bytes[$offset + 6] -left 8) -bor $bytes[$offset + 7]
                        }
                        {$_ -eq '3' -and $OUI -eq '00:80:C2'} { $VLANID    += (Invoke-BitShift($bytes[$offset + 6] -band 0xf) -Right 8) -bor $bytes[$offset + 7] }
                        {$_ -eq '4' -and $OUI -eq '00:12:0f'} { $FrameSize  = (Invoke-BitShift $bytes[$offset + 6] -left 8) -bor $bytes[$offset + 7] }
                        {$_ -eq '11' -and $OUI -eq '00:80:C2'} {
                            # Possible that more than one is enabled, so need to grab all of these
                            # Uses exactly 1 byte to define the state of each PFC priority
                            # The first 4 bits define upper range of priority e.g. 0001 = Priority 4 Enabled
                            # The last 4 bits define lower range of priority e.g. 1000 = Priority 3 Enabled

                            $thisByte = "{0:D2}" -f $bytes[$offset + 7]
                            # HigherBits = the left-most values in the last byte; LowerBits = the right-most values in the last byte
                            $HigherBits = -join $thisByte.ToString()[0] # -join operates as a substring
                            $LowerBits  = -join $thisByte.ToString()[1] # -join operates as a substring

                            $PFC = @()
                            if ($HigherBits -ne 0) {
                                $HigherPriority = [enum]::GetValues([PFC_higher]) | Where-Object {$_.value__ -band $HigherBits}
                                foreach ($priority in $HigherPriority) { $PFC += $priority.toString() }
                            }

                            if ($LowerBits -ne 0)  {
                                $LowerPriority  = [enum]::GetValues([PFC_lower]) | Where-Object {$_.value__ -band $LowerBits}
                                foreach ($priority in $LowerPriority) { $PFC += $priority.toString() }
                            }
                        }
                    }
                }
            }

            $offset = $offset + $Length + 2
        }

        # Set defaults in case the switch doesn't provide the information and guide the customer in their troubleshooting
        if (-not($PortDescription)) { $PortDescription = 'Information Not Provided By Switch' }
        if (-not($SystemName)) { $SystemName = 'Information Not Provided By Switch' }
        if (-not($NativeVLAN)) { $NativeVLAN = 'Information Not Provided By Switch' }
        if (-not($VLANID))     { $VLANID = 'Information Not Provided By Switch' }
        if (-not($FrameSize))  { $FrameSize  = 'Information Not Provided By Switch' }
        if (-not($PFC)) { $PFC  = 'Information Not Provided By Switch' }

        $Table += [ordered] @{
            InterfaceName   = (Get-NetAdapter -InterfaceIndex $thisEvent.Properties[0].Value).Name
            InterfaceIndex  = $thisEvent.Properties[0].Value
            DateTime        = $thisEvent.TimeCreated

            Destination     = $Destination # Mandatory
            sourceMac       = $sourceMac   # Mandatory
            EtherType       = $EtherType   # Mandatory
            ChassisID       = $ChassisID   # Mandatory

            PortDescription = $PortDescription # Optional
            SystemName      = $SystemName      # Optional

            NativeVLAN = $NativeVLAN        # IEEE 802.1 TLV:127 Subtype:1
            VLANID     = $VLANID            # IEEE 802.1 TLV:127 Subtype:3
            FrameSize  = $FrameSize         # IEEE 802.3 TLV:127 Subtype:4
            PFC        = $PFC | Sort-Object # IEEE 802.1 TLV:127 Subtype:11

            Bytes = $bytes # Raw Data from Packet
        }
    }

    Return $Table
}
#endregion LLDP

#region HostMap
Function Convert-CIDRToMask {
    param (
        [Parameter(Mandatory = $true)]
        [int] $PrefixLength
    )

    $bitString = ('1' * $prefixLength).PadRight(32, '0')

    [String] $MaskString = @()

    for($i = 0; $i -lt 32; $i += 8){
        $byteString = $bitString.Substring($i,8)
        $MaskString += "$([Convert]::ToInt32($byteString, 2))."
    }

    Return $MaskString.TrimEnd('.')
}

Function Convert-MaskToCIDR {
    param (
        [Parameter(Mandatory = $true)]
        [IPAddress] $SubnetMask
    )

    [String] $binaryString = @()
    $SubnetMask.GetAddressBytes() | ForEach-Object { $binaryString += [Convert]::ToString($_, 2) }

    Return $binaryString.TrimEnd('0').Length
}

Function Convert-IPv4ToInt {
    Param (
        [Parameter(Mandatory = $true)]
        [IPAddress] $IPv4Address
    )

    $bytes = $IPv4Address.GetAddressBytes()

    Return [System.BitConverter]::ToUInt32($bytes,0)
}

Function Convert-IntToIPv4 {
    Param (
        [Parameter(Mandatory = $true)]
        [uint32]$Integer
    )

    $bytes = [System.BitConverter]::GetBytes($Integer)

    Return ([IPAddress]($bytes)).ToString()
}

Class InterfaceDetails {
    [String] $IPAddress
    [String] $SubnetMask
    [String] $PrefixLength

    [String] $Network
    [String] $Subnet
    [String] $VLAN

    [string] $NetAdapterHostVNICName
    [string] $VMNetworkAdapterName
}
#endregion Host Map

#endregion Helper Functions

#region Exportable
function Test-FabricInfo {
    <#
    .SYNOPSIS
        Verifies prerequisites to running the other cmdlets in this module
 
    .EXAMPLE
        Test-FabricInfo
 
    .NOTES
        Author: Windows Core Networking team @ Microsoft
 
        Please file issues on GitHub @ GitHub.com/Microsoft/DataCenterBridging
 
    .LINK
        Windows Networking Blog : https://aka.ms/MSFTNetworkBlog
    #>


    [CmdletBinding(DefaultParameterSetName = 'InterfaceNames')]
    param (
        [Parameter(Mandatory=$true, ParameterSetName = 'InterfaceNames', Position=0)]
        [String[]] $InterfaceNames,

        [Parameter(Mandatory=$true, ParameterSetName = 'SwitchName')]
        [String] $SwitchName
    )

    $pass = '+'
    $fail = '-'
    $testsFailed = 0

    #region InterfaceNames
    If ($PSBoundParameters.ContainsKey('SwitchName')) {
        $VMSwitchTeam = Get-VMSwitchTeam -Name $SwitchName -ErrorAction SilentlyContinue

        if ($VMSwitchTeam) { $Interfaces = Get-Interfaces -SwitchName $SwitchName }
        Else { Write-Host "`'$SwitchName`' is not a Switch Embedded Team" -ForegroundColor Red ; break }
    }
    Elseif ($PSBoundParameters.ContainsKey('InterfaceNames')) {
        $NetAdapters = Get-NetAdapter -Name $InterfaceNames -ErrorAction SilentlyContinue
        # Not sure I understand this PowerShell funkyness but if I have only 1 adapter, the 'Count' Method is not available
        # Therefore, we need to check if there's only 1 interface name and make sure there's an entry in NetAdapters
        # Or check that there are more than 1 adapter

        if ($NetAdapters.Count) { $AdapterCount = $NetAdapters.Count }
        elseif ($NetAdapters) { $AdapterCount = 1 }
        else { $AdapterCount = 0 }

        If (-not($InterfaceNames.Count -eq $AdapterCount)) {
            if ($NetAdapters) {
                foreach ($Adapter in ($InterfaceNames -notmatch $NetAdapters.Name)) {
                    Write-Host "The interface `'$Adapter`' was not found" -ForegroundColor Red
                }
            }
            Else { Write-Host "No interfaces found with the specified names" -ForegroundColor Red }

            break
        }
        Else { $Interfaces = Get-Interfaces -InterfaceNames $InterfaceNames }
    }

    $remainingIndexes = $Interfaces.ifIndex

    foreach ($interface in $Interfaces) {
        if ($interface.Status -eq 'Up') { $PassFail = $pass }
        Else { $PassFail = $fail; $testsFailed ++ }

        if ($PassFail -eq $pass) { Write-Host "[$PassFail] Is Up: $($interface.Name)" }
        else { Write-Host "[$PassFail] Is Up: $($interface.Name)" -ForegroundColor Red }

        Remove-Variable PassFail -ErrorAction SilentlyContinue

        if ($Interface.MediaType -eq '802.3') { $PassFail = $pass }
        Else { $PassFail = $fail; $testsFailed ++ }
        
        if ($PassFail -eq $pass) { Write-Host "[$PassFail] Is MediaType 802.3: $($interface.Name)" }
        else { Write-Host "[$PassFail] Is MediaType 802.3: $($interface.Name)" -ForegroundColor Red }

        Remove-Variable PassFail -ErrorAction SilentlyContinue
    }
    #endregion InterfaceNames

    #region LLDP RSAT Tools Install
    $computerInfo = Get-ComputerInfo -Property WindowsInstallationType, csmodel -ErrorAction SilentlyContinue

    if ($computerInfo.WindowsInstallationType -eq 'Client') {
        $isLLDPInstalled = Get-WindowsCapability -Online -ErrorAction SilentlyContinue | Where-Object Name -like *LLDP*
        
        if ($isLLDPInstalled.State -eq 'Installed') { Write-Host "[$Pass] Is Installed: RSAT Data Center Bridging LLDP Tools" }
        else { 
            Write-Host "[$Fail] Is Installed: RSAT Data Center Bridging LLDP Tools" -ForegroundColor Red
            $testsFailed ++
        }
    }
    else {
        $isLLDPInstalled = Get-WindowsFeature 'RSAT-DataCenterBridging-LLDP-Tools' -ErrorAction SilentlyContinue

        if ($isLLDPInstalled.InstallState -eq 'Installed') { Write-Host "[$Pass] Is Installed: RSAT-DataCenterBridging-LLDP-Tools" }
        else {
            Write-Host "[$Fail] Is Installed: RSAT-DataCenterBridging-LLDP-Tools" -ForegroundColor Red
            $testsFailed ++
        }
    }
    
    Remove-Variable PassFail, isLLDPInstalled -ErrorAction SilentlyContinue

    if ($computerInfo.CsModel -ne 'Virtual Machine') {
        Write-Host "[$Pass] Is Physical Host: True"
    }
    else {
        Write-Host "[$Fail] Is Physical Host: False" -ForegroundColor Red
        $testsFailed ++
    }
    #endregion

    #region Event log exists and is enabled
    $isEvtLogEnabled = Get-WinEvent -ListLog 'Microsoft-Windows-LinkLayerDiscoveryProtocol/Diagnostic' -ErrorAction SilentlyContinue

    if ($isEvtLogEnabled) { $PassFail = $pass }
    Else { $PassFail = $fail; $testsFailed ++ }

    if ($PassFail -eq $pass) { Write-Host "[$PassFail] Is Found: Event Log (Microsoft-Windows-LinkLayerDiscoveryProtocol/Diagnostic)" }
    else { Write-Host "[$PassFail] Is Found: Event Log (Microsoft-Windows-LinkLayerDiscoveryProtocol/Diagnostic)" -ForegroundColor Red }

    Remove-Variable PassFail -ErrorAction SilentlyContinue

    if ($isEvtLogEnabled.IsEnabled) { $PassFail = $pass }
    Else { $PassFail = $fail; $testsFailed ++ }

    if ($PassFail -eq $pass) { Write-Host "[$PassFail] Is Enabled: Event Log (Microsoft-Windows-LinkLayerDiscoveryProtocol/Diagnostic)" }
    else { Write-Host "[$PassFail] Is Enabled: Event Log (Microsoft-Windows-LinkLayerDiscoveryProtocol/Diagnostic)" -ForegroundColor Red }
    
    Remove-Variable PassFail -ErrorAction SilentlyContinue

    if ($isEvtLogEnabled.FileSize -lt ($isEvtLogEnabled.MaximumSizeInBytes * .9)) { $PassFail = $pass }
    Else { $PassFail = $fail; $testsFailed ++ }

    if ($PassFail -eq $pass) { Write-Host "[$PassFail] Is NOT Full: Microsoft-Windows-LinkLayerDiscoveryProtocol/Diagnostic" }
    else { Write-Host "[$PassFail] Is NOT Full: Microsoft-Windows-LinkLayerDiscoveryProtocol/Diagnostic" -ForegroundColor Red }
    Remove-Variable PassFail -ErrorAction SilentlyContinue
    #endregion

    #region Get Fabric Info
    $global:IndexesMissingEvents = $Null
    $event = Get-LLDPEvents -RemainingIndexes $remainingIndexes

    $remainingIndexes | ForEach-Object {
        $thisRemainingIndex = $_

        if ($thisRemainingIndex -notin $global:IndexesMissingEvents) {
            $PassFail = $Pass

            Write-Host "[$PassFail] Is Found: LLDP Packet for index $thisRemainingIndex"
            Remove-Variable PassFail -ErrorAction SilentlyContinue
        }
        Else {
            $testsFailed ++
            $PassFail = $fail

            Write-Host "[$PassFail] Is Found: LLDP Packet for index $thisRemainingIndex" -ForegroundColor Red
            Remove-Variable PassFail -ErrorAction SilentlyContinue
        }
    }
    #endregion

    if ($testsFailed -eq 0) { Write-Host 'Successfully passed all tests' -ForegroundColor Green }
    else { Write-Host "`rFailed $testsFailed tests. Please review the output before continuing." -ForegroundColor Red }
}

function Enable-FabricInfo {
    param (
        [Parameter(Mandatory=$true, ParameterSetName = 'InterfaceNames', Position=0)]
        [String[]] $InterfaceNames,

        [Parameter(Mandatory=$true, ParameterSetName = 'SwitchName')]
        [String] $SwitchName
    )

    $computerInfo = Get-ComputerInfo -Property WindowsInstallationType, csmodel -ErrorAction SilentlyContinue

    if ($computerInfo.CsModel -eq 'Virtual Machine') { throw 'Cannot be enabled on a virtual machine.' }
    
    if ($computerInfo.WindowsInstallationType -eq 'Client') {
        $isLLDPInstalled = Get-WindowsCapability -Online -ErrorAction SilentlyContinue | Where-Object Name -like *LLDP*

        if ($isLLDPInstalled.State -ne 'Installed') { Add-WindowsCapability -Name Rsat.LLDP.Tools~~~~0.0.1.0 -Online }
    }
    else {
        $isLLDPInstalled = Get-WindowsFeature -Name RSAT-DataCenterBridging-LLDP-Tools -ErrorAction SilentlyContinue

        if ($isLLDPInstalled.State -ne 'Installed') { Install-WindowsFeature -Name RSAT-DataCenterBridging-LLDP-Tools }
    }

    # Enable NetLLDPAgent and get logs
    $LLDPLog = Get-WinEvent -ListLog Microsoft-Windows-LinkLayerDiscoveryProtocol/Diagnostic

    if ($LLDPLog.FileSize -gt ($LLDPLog.MaximumSizeInBytes * .9)) {
        $LLDPLog.IsEnabled = $false
        $LLDPLog.SaveChanges()
    }

    if ($LLDPLog.IsEnabled -eq $false) {
        $LLDPLog.IsEnabled = $true
        $LLDPLog.SaveChanges()
    }

    If ($PSBoundParameters.ContainsKey('SwitchName')) {
        $VMSwitch     = Get-VMSwitch -Name $SwitchName -ErrorAction SilentlyContinue
        $VMSwitchTeam = Get-VMSwitchTeam -Name $SwitchName -ErrorAction SilentlyContinue

        if ($VMSwitch -and $VMSwitchTeam) { $Interfaces = Get-Interfaces -SwitchName $SwitchName }
        Else { Write-Host "`'$SwitchName`' is not a Switch Embedded Team" -ForegroundColor Red ; break }
    }
    Elseif ($PSBoundParameters.ContainsKey('InterfaceNames')) {
        $NetAdapters = Get-NetAdapter -Name $InterfaceNames -ErrorAction SilentlyContinue
        # Not sure I understand this PowerShell funkyness but if I have only 1 adapter, the 'Count' Method is not available
        # Therefore, we need to check if there's only 1 interface name and make sure there's an entry in NetAdapters
        # Or check that there are more than 1 adapter

        if ($NetAdapters.Count) { $AdapterCount = $NetAdapters.Count }
        elseif ($NetAdapters) { $AdapterCount = 1 }
        else { $AdapterCount = 0 }

        If (-not($InterfaceNames.Count -eq $AdapterCount)) {
            if ($NetAdapters) {
                foreach ($Adapter in ($InterfaceNames -notmatch $NetAdapters.Name)) {
                    Write-Host "The interface `'$Adapter`' was not found" -ForegroundColor Red
                }
            }
            Else { Write-Host "No interfaces found with the specified names" -ForegroundColor Red }

            break
        }
        Else { $Interfaces = Get-Interfaces -InterfaceNames $InterfaceNames }
    }

    $remainingIndexes = $Interfaces.ifIndex

    Enable-NetLldpAgent -InterfaceIndex $remainingIndexes

    Write-Verbose 'LLDP has been enabled for the specified interfaces; LLDP packets are typically sent every 30 seconds'
    Write-Host    'Please run Test-FabricInfo to determine if all requirements have been met'
}

function Get-FabricInfo {
    param (
        [Parameter(Mandatory=$true, ParameterSetName = 'InterfaceNames', Position=0)]
        [String[]] $InterfaceNames,

        [Parameter(Mandatory=$true, ParameterSetName = 'SwitchName')]
        [String] $SwitchName
    )

    If ($PSBoundParameters.ContainsKey('SwitchName')) {
        $VMSwitch     = Get-VMSwitch -Name $SwitchName -ErrorAction SilentlyContinue
        $VMSwitchTeam = Get-VMSwitchTeam -Name $SwitchName -ErrorAction SilentlyContinue

        if ($VMSwitch -and $VMSwitchTeam) { $Interfaces = Get-Interfaces -SwitchName $SwitchName }
        Else { Write-Host "`'$SwitchName`' is not a Switch Embedded Team" -ForegroundColor Red ; break }
    }
    Elseif ($PSBoundParameters.ContainsKey('InterfaceNames')) {
        $NetAdapters = Get-NetAdapter -Name $InterfaceNames -ErrorAction SilentlyContinue
        # Not sure I understand this PowerShell funkyness but if I have only 1 adapter, the 'Count' Method is not available
        # Therefore, we need to check if there's only 1 interface name and make sure there's an entry in NetAdapters
        # Or check that there are more than 1 adapter

        if ($NetAdapters.Count) { $AdapterCount = $NetAdapters.Count }
        elseif ($NetAdapters) { $AdapterCount = 1 }
        else { $AdapterCount = 0 }

        If (-not($InterfaceNames.Count -eq $AdapterCount)) {
            if ($NetAdapters) {
                foreach ($Adapter in ($InterfaceNames -notmatch $NetAdapters.Name)) {
                    Write-Host "The interface `'$Adapter`' was not found" -ForegroundColor Red
                }
            }
            Else { Write-Host "No interfaces found with the specified names" -ForegroundColor Red }

            break
        }
        Else { $Interfaces = Get-Interfaces -InterfaceNames $InterfaceNames }
    }

    $remainingIndexes = $Interfaces.ifIndex

    $event = Get-LLDPEvents -RemainingIndexes $remainingIndexes

    if ($event.count -ne $remainingIndexes.Count) {
        Write-Host "Could not find an LLDP Packet one or more of the interfaces specified. Please run Test-FabricInfo." -ForegroundColor Red
    }
    Else { $InterfaceTable = Parse-LLDPPacket -Events $event }

    #Convert To/From JSON to make a simple object with property names
    $JsonTable = $InterfaceTable | ConvertTo-Json
    $InterfaceTable = $JsonTable | ConvertFrom-Json

    Remove-Variable jsonTable -ErrorAction SilentlyContinue

    $ChassisGroups = $InterfaceTable | Group-Object ChassisID
    #$portOrder = $ChassisGroups.Group | Sort sourceMac | Select InterfaceName, InterfaceIndex, ChassisID, SourceMac

    $InterfaceDetails = @()
    $HostNetAdapters = @()
    $interfaceMap = @()

    $HostVNICTeamMap = Get-VMNetworkAdapterTeamMapping -ManagementOS | Where-Object NetAdapterName -in $Interfaces.Name
    $Interfaces | ForEach-Object {
        $thisInterface = $_
        $InterfaceBinding = Get-NetAdapterBinding -Name $thisInterface.Name -ComponentID ms_tcpip, vms_pp

        if (($InterfaceBinding | Where-Object ComponentID -eq 'vms_pp').Enabled -eq $true ) {
            $thisHostVNICParentAdapter = ($HostVNICTeamMap | Where-Object NetAdapterName -eq $thisInterface.Name).ParentAdapter
            $HostNetAdapterWithIP = Get-NetAdapter -Name $thisHostVNICParentAdapter.Name
        }
        Else { $HostNetAdapterWithIP = $thisInterface }

        $HostNetAdapterWithIP | ForEach-Object {
            $thisHostNetAdapter = $_
            $thisIP = Get-NetIPAddress -InterfaceIndex $thisHostNetAdapter.ifIndex -AddressFamily IPv4

            if ($thisIP) {
                $thisHostNetAdapterInterfaceDetails = [InterfaceDetails]::new()

                $thisHostNetAdapterInterfaceDetails.IpAddress    = $thisIP.IPAddress
                $thisHostNetAdapterInterfaceDetails.PrefixLength = $thisIP.PrefixLength
                $thisHostNetAdapterInterfaceDetails.SubnetMask = Convert-CIDRToMask -PrefixLength $thisIP.PrefixLength

                $SubNetInInt = Convert-IPv4ToInt -IPv4Address $thisHostNetAdapterInterfaceDetails.SubnetMask
                $IPInInt     = Convert-IPv4ToInt -IPv4Address $thisHostNetAdapterInterfaceDetails.IPAddress
                $thisHostNetAdapterInterfaceDetails.Network = Convert-IntToIPv4 -Integer ($SubNetInInt -band $IPInInt)
                $thisHostNetAdapterInterfaceDetails.Subnet = "$($thisHostNetAdapterInterfaceDetails.Network)/$($thisHostNetAdapterInterfaceDetails.PrefixLength)"

                # Device is virtual
                if ($thisHostNetAdapter.ConnectorPresent -eq $false) {
                    if ($thisHostVNICParentAdapter.IsolationSetting.IsolationMode -eq 'VLAN') {
                        $thisHostNetAdapterInterfaceDetails.VLAN = $thisHostVNICParentAdapter.IsolationSetting.DefaultIsolationID
                    }

                    Switch ($thisHostVNICParentAdapter.VlanSetting.OperationMode) {
                        'Access' { $thisHostNetAdapterInterfaceDetails.VLAN = $thisHostVNICParentAdapter.VlanSetting.AccessVLANID }
                        'Trunk' { $thisHostNetAdapterInterfaceDetails.VLAN = $thisHostVNICParentAdapter.VlanSetting.NativeVlanId }
                    }

                    $thisHostNetAdapterInterfaceDetails.VMNetworkAdapterName = $thisHostVNICParentAdapter.Name
                    $thisHostNetAdapterInterfaceDetails.NetAdapterHostVNICName = $HostNetAdapterWithIP.Name
                }
                Else {
                    $thisHostNetAdapterInterfaceDetails.VLAN = (Get-NetAdapterAdvancedProperty -Name $thisHostNetAdapter.Name -RegistryKeyword VLANID -ErrorAction SilentlyContinue).RegistryValue
                }

                $interfaceDetails = [ordered] @{
                    IPAddress    = $thisHostNetAdapterInterfaceDetails.IPAddress
                    SubnetMask   = $thisHostNetAdapterInterfaceDetails.SubnetMask
                    PrefixLength = $thisHostNetAdapterInterfaceDetails.PrefixLength
                    Network      = $thisHostNetAdapterInterfaceDetails.Network

                    Subnet = $thisHostNetAdapterInterfaceDetails.Subnet
                    VLAN   = $thisHostNetAdapterInterfaceDetails.VLAN

                    InterfaceName  = $thisInterface.Name
                    InterfaceIndex = $thisInterface.IfIndex

                    NetAdapterHostVNICName = $thisHostNetAdapterInterfaceDetails.NetAdapterHostVNICName
                    VMNetworkAdapterName   = $thisHostNetAdapterInterfaceDetails.VMNetworkAdapterName
                }

                #Convert To/From JSON to make a simple object with property names
                $JsonTable = $interfaceDetails | ConvertTo-Json
                $interfaceDetails = $JsonTable | ConvertFrom-Json

                $interfaceMap += $interfaceDetails
            }
        }
    }

    $Mapping = @{}
    $interfaces | ForEach-Object {
        $thisInterfaceName = $_.Name
        $Mapping.$thisInterfaceName += @{
            Fabric = $InterfaceTable | Where-Object InterfaceName -eq $thisInterfaceName
            InterfaceDetails = $interfaceMap | Where-Object InterfaceName -eq $thisInterfaceName
        }
    }

    $Mapping += @{ ChassisGroups = $ChassisGroups }

    return $Mapping
}

function Start-FabricCapture {
    <#
    .SYNOPSIS
        Performs a packet capture of LLDP packets for the specified interfaces
 
    .EXAMPLE
        Start-FabricCapture
 
    .NOTES
        Author: Windows Core Networking team @ Microsoft
 
        Please file issues on GitHub @ GitHub.com/Microsoft/DataCenterBridging
 
    .LINK
        Windows Networking Blog : https://aka.ms/MSFTNetworkBlog
    #>


    [CmdletBinding(DefaultParameterSetName = 'InterfaceNames')]
    param (
        [Parameter(Mandatory=$true, ParameterSetName = 'InterfaceNames', Position=0)]
        [String[]] $InterfaceNames,

        [Parameter(Mandatory=$true, ParameterSetName = 'SwitchName')]
        [String] $SwitchName,

        #Typical LLDP interval is 30 seconds, so adding 1 to ensure we capture something
        [Parameter(Mandatory=$false)]
        [int] $CaptureTime = 31
    )

    #region InterfaceNames
    If ($PSBoundParameters.ContainsKey('SwitchName')) {
        $VMSwitchTeam = Get-VMSwitchTeam -Name $SwitchName -ErrorAction SilentlyContinue

        if ($VMSwitchTeam) { $Interfaces = Get-Interfaces -SwitchName $SwitchName }
        Else { Write-Host "`'$SwitchName`' is not a Switch Embedded Team" -ForegroundColor Red ; break }
    }
    Elseif ($PSBoundParameters.ContainsKey('InterfaceNames')) {
        $NetAdapters = Get-NetAdapter -Name $InterfaceNames -ErrorAction SilentlyContinue
        # Not sure I understand this PowerShell funkyness but if I have only 1 adapter, the 'Count' Method is not available
        # Therefore, we need to check if there's only 1 interface name and make sure there's an entry in NetAdapters
        # Or check that there are more than 1 adapter

        if ($NetAdapters.Count) { $AdapterCount = $NetAdapters.Count }
        elseif ($NetAdapters) { $AdapterCount = 1 }
        else { $AdapterCount = 0 }

        If (-not($InterfaceNames.Count -eq $AdapterCount)) {
            if ($NetAdapters) {
                foreach ($Adapter in ($InterfaceNames -notmatch $NetAdapters.Name)) {
                    Write-Host "The interface `'$Adapter`' was not found" -ForegroundColor Red
                }
            }
            Else { Write-Host "No interfaces found with the specified names" -ForegroundColor Red }

            break
        }
        Else { $Interfaces = Get-Interfaces -InterfaceNames $InterfaceNames }
    }
    #endregion InterfaceNames

    <# Won't use the powershell cmdlets because we want to capture from Interface pNIC01 only
     # LLDP uses multicast to send the port data and therefore src/dst address doesn't match to the pNIC#>


    # Ensuring no previous messes exist
    $interfaceNames | ForEach-Object {
        netsh.exe trace stop session=$_ | Out-Null
        Get-NetEventSession | Stop-NetEventSession -ErrorAction SilentlyContinue
        Get-NetEventSession | Remove-NetEventSession -ErrorAction SilentlyContinue
    }

    New-Item -Path "$PSScriptRoot\Capture" -ItemType Directory -Force | Out-Null
    Write-Host "Beginning capture..."

    $StartTime = Get-Date -format:'ddHHmmss'
    $interfaceNames | ForEach-Object {
        # netsh trace show CaptureFilterHelp has a ton of filter information
        netsh.exe trace start CaptureInterface=$_ Ethernet.Type=0x88cc capture=yes session=$_ correlation=disabled report=disabled PacketTruncateBytes=65000 tracefile="$PSScriptRoot\capture\$StartTime$_.etl" | Out-Null
    }

    Write-Host "Sleeping during capture for $CaptureTime seconds"
    Start-Sleep -Seconds $CaptureTime

    Write-Host 'Capture complete'
    Write-Host 'Converting capture to WireShark format'

    $interfaceNames | ForEach-Object {
        # netsh trace show CaptureFilterHelp has a ton of filter information
        netsh.exe trace stop session=$_ | Out-Null

        & "$PSScriptRoot\capture\etl2pcapng.exe" "$PSScriptRoot\capture\$StartTime$_.etl" "$PSScriptRoot\capture\$StartTime$_.pcapng" | Out-Null

        $PCAPNG = Get-Item "$PSScriptRoot\capture\$StartTime$_.pcapng" -ErrorAction SilentlyContinue

        if ($PCAPNG) {
            Write-Host "`rETL Capture is available at: $("$PSScriptRoot\capture\$StartTime$_.etl")"
            Write-Host "Wireshark Capture (.pcapng) is available at: $("$PSScriptRoot\capture\$StartTime$_.pcapng")"
        }
        else {
            throw "`rWireshark Capture (.pcapng) was not successfully generated.
                   `rVerify you the latest C runtime is installed on this system (https://aka.ms/VCRedist) and try again.
                   `r`rOnce resolved, you can run Start-FabricCapture again, or manually convert the file using the command:`r`r
                   $PSScriptRoot\capture\etl2pcapng.exe $PSScriptRoot\capture\$StartTime$_.etl $PSScriptRoot\capture\$StartTime$_.pcapng"

        }
    }
}
#endregion Exportable
#endregion FabricInfo