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[]] $InterfaceIndex,

        [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 }
    elseif ($InterfaceIndex) { $Interfaces = Get-NetAdapter -InterfaceIndex $InterfaceIndex }

    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 ($Null -ne $remainingIndexes) {
        #$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

        # search for matches using the appropriate filter
        [hashtable]$eventFilter = @{LogName='Microsoft-Windows-LinkLayerDiscoveryProtocol/Diagnostic'; ID=10041; StartTime=$(Get-Date).AddMinutes(-5)}

        [array]$CuratedEvents = Get-WinEvent -FilterHashtable $eventFilter -Oldest -EA SilentlyContinue | Sort-Object TimeCreated -Descending

        foreach ($thisEvent in $CuratedEvents)
        {
            #$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 ($null -eq $remainingIndexes) {break enoughEvents}
        }

        break enoughEvents
    }

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

    return $EventPerInterface
}


function Convert-Bytes2IP
{
    [CmdletBinding()]
    param (
        [Parameter()]
        [byte[]]
        $ipBytes
    )

    # get them bits
    $IPBinary = (([System.BitConverter]::ToString($ipBytes).Replace('-','')).ToCharArray() | & { process { [System.Convert]::ToString([byte]"0x$_",2).PadLeft(4,'0') } }) -join ''

    try
    {
        $IP = ([System.Net.IPAddress]"$([System.Convert]::ToInt64($IPBinary,2))").IPAddressToString
    }
    catch
    {
        Write-Warning "Convert-Bytes2IP - Failed to convert bytes to IP address: $_"
        return $null
    }

    return $IP
}


# https://www.ieee802.org/1/files/public/docs2002/LLDP%20Overview.pdf

function Get-LldpTlv
{
    [CmdletBinding()]
    param (
        [Parameter()]
        [byte[]]
        $tlvBytes
    )

    Write-Verbose "Get-LldpTlv - Begin"
    Write-Verbose "Get-LldpTlv - Bytes: $($tlvBytes -join ", ")"

    # skip processing if we hit the end
    if ($tlvBytes[0] -eq 0)
    {
        return ([PSCustomObject]@{
            Type = 0
            Len  = 0
        })
    }

    # get them bits
    $rawBits = (([System.BitConverter]::ToString($tlvBytes).Replace('-','')).ToCharArray() | & { process { [System.Convert]::ToString([byte]"0x$_",2).PadLeft(4,'0') } }) -join ''

    Write-Verbose "Get-LldpTlv - bits: $rawBits"

    $tlvType = [convert]::ToInt32($rawBits.Substring(0,7), 2)
    Write-Verbose "Get-LldpTlv - Type: $tlvType"

    $tlvLen = [convert]::ToInt32($rawBits.Substring(7,9), 2)
    Write-Verbose "Get-LldpTlv - Length: $tlvLen"

    Write-Verbose "Get-LldpTlv - End"
    return ([PSCustomObject]@{
        Type = $tlvType
        Len  = $tlvLen
    })
}


function Get-TlvChassisId
{
    [CmdletBinding()]
    param (
        [Parameter()]
        [byte[]]
        $chasBytes
    )

    # get the Chassis ID Subtype
    [int]$chasIdType = "0x$($chasBytes[0])"

    switch -Regex ($chasIdType)
    {
        "[1-3]"
        {
            # convert bytes to string
            return ( [System.Text.Encoding]::ASCII.GetString($chasBytes[1..($chasBytes.Length - 1)]) )
        }
        "4"
        {
            # MAC address
            return ("{0:X2}:{1:X2}:{2:X2}:{3:X2}:{4:X2}:{5:X2}" -f $chasBytes[1..($chasBytes.Length - 1)])
        }

        "5"
        {
            # management address
            return (Convert-Bytes2IP ($chasBytes[1..($chasBytes.Length - 1)]) )
        }

        Default
        {
            Write-Warning "Unknown Chassis ID type: $chasIdType"
            return "Unknown"
        }
    }
}


function Get-TlvPortId
{
    [CmdletBinding()]
    param (
        [Parameter()]
        [byte[]]
        $portBytes
    )

    # get the Chassis ID Subtype
    [int]$portIdType = "0x$($portBytes[0])"

    Write-Verbose "Get-TlvPortId - Port Type: $portIdType"

    switch -Regex ($portIdType)
    {
        "[1-2]|[5]"
        {
            # convert bytes to string
            return ( [System.Text.Encoding]::ASCII.GetString($portBytes[1..($portBytes.Length - 1)]) )
        }
        "3"
        {
            # MAC address
            return ("{0:X2}:{1:X2}:{2:X2}:{3:X2}:{4:X2}:{5:X2}" -f $portBytes[1..($portBytes.Length - 1)])
        }

        "4"
        {
            # management address
            return (Convert-Bytes2IP ($portBytes[1..($portBytes.Length - 1)]) )
        }

        Default
        {
            Write-Warning "Unknown Port ID type: $portIdType"
            return "Unknown"
        }
    }
}


function Get-LldpPfc
{
    [CmdletBinding()]
    param (
        [Parameter()]
        [byte[]]
        $portBytes
    )

    $rawBits = (([System.BitConverter]::ToString($portBytes).Replace('-','')).ToCharArray() | & { process { [System.Convert]::ToString([byte]"0x$_",2).PadLeft(4,'0') } }) -join ''

    # PFC flags to Int
    $intPFC = [convert]::ToInt32($rawBits.Substring(8), 2)

    # only return Priorities for now
    return ([enum]::GetValues([PFC_Priorities]) | Where-Object {$_.value__ -band $intPFC} | ForEach-Object {$_.ToString()})

}


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

        [Parameter(Mandatory=$true, ParameterSetName = 'InterfaceIndex')]
        [UInt32[]] $InterfaceIndex,

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

    # LBFO detection
    [array]$isLbfoUsed = Get-NetLbfoTeam -EA SilentlyContinue
    if ($isLbfoUsed)
    {
        # throw a warning when the interface or switch is using LBFO
        if ($PSBoundParameters.ContainsKey('SwitchName'))
        {
            # skip this check if the switch is a teamed interface (SET)
            $vSwitch = Get-VMSwitch -SwitchName $SwitchName -EA SilentlyContinue

            if (-NOT $vSwitch)
            {
                return (Write-Error "No switch found named $SwitchName." -EA Stop)
            }
            elseif ($vSwitch.EmbeddedTeamingEnabled -eq $false) 
            {
                $switchAdapterDesc = $vSwitch | ForEach-Object { $_.NetAdapterInterfaceDescription }
                $switchAdapterName = Get-NetAdapter -InterfaceDescription $switchAdapterDesc -EA SilentlyContinue | ForEach-Object Name

                if ($isLbfoUsed.Name -contains $switchAdapterName)
                {
                    $LbfoUsed = Get-NetAdapter -InterfaceAlias $switchAdapterName
                }
            }
        }
        elseif ($PSBoundParameters.ContainsKey('InterfaceNames')) 
        {
            [String[]]$lbfoTeamMem = Get-NetLbfoTeamMember -EA SilentlyContinue | ForEach-Object Name

            $LbfoUsed = $InterfaceNames | ForEach-Object { if ($lbfoTeamMem -contains $_) { $_ } }
        }
        elseif ($PSBoundParameters.ContainsKey('InterfaceIndex')) 
        {
            [String[]]$lbfoTeamMem = Get-NetLbfoTeamMember -EA SilentlyContinue | ForEach-Object Name

            [string[]]$InterfaceNames = Get-NetAdapter -InterfaceIndex $InterfaceIndex -EA SilentlyContinue | ForEach-Object Name

            $LbfoUsed = $InterfaceNames | ForEach-Object { if ($lbfoTeamMem -contains $_) { $_ } }
        }
    }

    if ($LbfoUsed)
    {
        Write-Warning "LBFO Teams are not supported. DataCenterBrinding cmdlets may not operate correctly or provide inaccurate results. Microsoft recommends using Switch Embedded Teaming (SET): https://aka.ms/DownWithLBFO"
        return $true
    }

    # no LBFO detected
    return $false
}



function Parse-LLDPPacket {
    param ($Events)

    $Table = @()

    [hashtable]$tlvTable = [ordered]@{
        End                  = 0
        ChassisId            = 1
        PortId               = 2
        TimeToLive           = 3
        PortDescription      = 4
        SystemName           = 5
        SystemDesc           = 6
        OrganizationSpecific = 127
    }

    [Flags()] enum PFC_Priorities {
        Priority0 = 1
        Priority1 = 2
        Priority2 = 4
        Priority3 = 8
        Priority4 = 16
        Priority5 = 32
        Priority6 = 64
        Priority7 = 128
    }

    foreach ($thisEvent in $Events)
    {
        $bytes = $thisEvent.Properties[3].Value

        # the LLDP header always starts at offset 14
        $offset = 14

        # keeps track of VLAN IDs
        [int[]]$VLANID = @()

        # parse the ethernet header
        $ethDst = "{0:X2}:{1:X2}:{2:X2}:{3:X2}:{4:X2}:{5:X2}" -f $bytes[0..5]
        $ethSrc = "{0:X2}:{1:X2}:{2:X2}:{3:X2}:{4:X2}:{5:X2}" -f $bytes[6..11]
        $ethType = "0x$([BitConverter]::ToString($bytes[12]) + [BitConverter]::ToString($bytes[13]))"


        ## parse the LLDP header ##

        # parse until the "End of LLDPDU" TLV is reached (00 00)
        $EndOfLLDPDU = $false

        while ($EndOfLLDPDU -eq $false)
        {
            # parse the TLV
            Write-Verbose "tlv header: $($bytes[$offset..($offset+1)] -join ", ")"
            $TLV = Get-LldpTlv $bytes[$offset..($offset+1)]

            # offset + 2 (TLV bytes) + TLV Length - 1 (to compensate for index starting at 0)
            $tlvEnd = $offset + 2 + $TLV.Len - 1

            switch ($TLV.Type)
            {
                #region
                $tlvTable.End
                {
                    $EndOfLLDPDU = $true
                    break
                }

                $tlvTable.ChassisId
                {
                    # just need the string Chassis ID back
                    $ChassisID = Get-TlvChassisId $bytes[($offset+2)..$tlvEnd]
                    break
                }

                $tlvTable.PortId
                {
                    $PortId = Get-TlvPortId $bytes[($offset+2)..$tlvEnd]
                    break
                }

                <#
                $tlvTable.TimeToLive
                {
                    [int]$tlvTTL = "0x$(($bytes[($offset+2)..$tlvEnd] | ForEach-Object { "{0:X2}" -f $_ }) -join '')"
                    break
                }
                #>


                $tlvTable.PortDescription
                {
                    $PortDescription = [System.Text.Encoding]::ASCII.GetString($bytes[($offset+2)..$tlvEnd])
                    break
                }

                $tlvTable.SystemName
                {
                    $SystemName = [System.Text.Encoding]::ASCII.GetString($bytes[($offset+2)..$tlvEnd])
                    break
                }

                $tlvTable.SystemDesc
                {
                    $SystemDesc = [System.Text.Encoding]::ASCII.GetString($bytes[($offset+2)..$tlvEnd])
                    break
                }
                #endregion

                $tlvTable.OrganizationSpecific
                {
                    <#
                        We only care about the following org codes and subtypes:
 
                            Code 00:12:0f (IEEE 802.3) - Subtype 0x04 (MTU)
                            Code 00:80:c2 (IEEE) - Subtype 0x0b (PFC)
                            Code 00:80:c2 (IEEE) - Subtype 0x01 (Default VLAN)
                            Code 00:80:c2 (IEEE) - Subtype 0x03 (Port VLANs|VLAN Name) ---> There can be multiple entries for this subtype, one for each advertised VLAN.
 
                    #>

                    # org code
                    $orgCode = "{0:X2}:{1:X2}:{2:X2}" -f $bytes[($offset+2)..($offset+4)]

                    switch ($orgCode)
                    {
                        "00:80:C2"
                        {
                            # Get the subtype
                            $subtype = $bytes[($offset+5)]

                            switch ($subtype)
                            {
                                1
                                {
                                    # Port VLAN ID (default) - can be up to 2 bytes (4096) in length
                                    #$NativeVLAN = Convert-Bytes2Int $bytes[($offset+6)..$tlvEnd]
                                    $NativeVLAN = [convert]::ToInt32( (($bytes[($offset+6)..$tlvEnd] | & { process { [System.Convert]::ToString($_,2).PadLeft(8,'0') } } ) -join ''), 2)
                                    break
                                }

                                3
                                {
                                    # VLAN ID = first 2 bytes after the subtype
                                    #$VLANID += Convert-Bytes2Int $bytes[($offset+6)..($offset+7)]
                                    $VLANID += [convert]::ToInt32( (($bytes[($offset+6)..($offset+7)] | & { process { [System.Convert]::ToString($_,2).PadLeft(8,'0') } } ) -join ''), 2)

                                    # 1 byte for the length of the name
                                    # but first make sure we aren't at the end of the TLV by checking if the last byte of the VLAN ID is less than tlvEnd
                                    # we don't care about the VLAN Name right now, but I'll leave this code in here in case we do in the future.
                                    <#
                                    if ( ($offset+7) -lt $tlvEnd )
                                    {
                                        # ($offset+8) thru $tlvEnd should be the string VLAN name. Skipping additional checks for speed.
                                        $vlanName = [System.Text.Encoding]::ASCII.GetString($bytes[($offset+8)..$tlvEnd])
                                    }
                                    #>


                                    break
                                }

                                11
                                {
                                    # get PFC stuff
                                    $PFC = Get-LldpPfc $bytes[($offset+6)..$tlvEnd]
                                }

                                default
                                {
                                    Write-Verbose "Ignoring Organization Unique Code 00:80:C2 (IEEE), subtype $subType."
                                    break
                                }
                            }
                            break
                        }

                        "00:12:0f"
                        {
                            # Get the subtype
                            $subtype = $bytes[($offset+5)]


                            switch ($subtype)
                            {
                                4
                                {
                                    # frame size (MTU)
                                    #$FrameSize = Convert-Bytes2Int $bytes[($offset+6)..($offset+7)]
                                    $FrameSize = [convert]::ToInt32( (($bytes[($offset+6)..($offset+7)] | & { process { [System.Convert]::ToString($_,2).PadLeft(8,'0') } } ) -join ''), 2)
                                }

                                default
                                {
                                    Write-Verbose "Ignoring Organization Unique Code 00:12:0f (IEEE 802.3), subtype $subType."
                                    break
                                }
                            }
                            break
                        }

                        default
                        {
                            Write-Verbose "Ignoring Organization Unique Code $orgCode."
                            break
                        }
                    }
                }

                default
                {
                    $knownTlv = ($tlvTable.GetEnumerator() | Where-Object Value -eq $TLV.Type)
                    if ($knownTlv) {
                        $tlvName = $knownTlv.Name
                    }

                    Write-Verbose "Ignoring TLV $($TLV.Type)$( if ($tlvName) { " ($tlvName)" })."
                    Remove-Variable tlvName, knownTlv -EA SilentlyContinue
                    break
                }
            }

            # increment offset to the start of the next TLV
            $offset = $tlvEnd + 1
        }


        # 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($PortId)) { $PortId = 'Information Not Provided By Switch' }
        if (-not($SystemName)) { $SystemName = 'Information Not Provided By Switch' }
        if (-not($SystemDesc)) { $SystemDesc = 'Information Not Provided By Switch' }
        if (-not($NativeVLAN)) { $NativeVLAN = 'Information Not Provided By Switch' }
        if (-not($VLANID))     { [string]$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     = $ethDst # Mandatory
            sourceMac       = $ethSrc   # Mandatory
            EtherType       = $ethType   # Mandatory
            ChassisID       = $ChassisID   # Mandatory
            PortID          = $PortId     # Mandatory

            PortDescription = $PortDescription # Optional
            SystemName      = $SystemName      # Optional
            SystemDesc      = $SystemDesc      # 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
        }

        # clean up to prevent bad output
        Remove-Variable thisEvent, ethDst, ethSrc, ethType, ChassisID, PortId, PortDescription, SystemName, SystemDesc, NativeVLAN, VLANID, FrameSize, PFC, bytes -EA SilentlyContinue
    }


    return $Table
}

<#
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 = 'InterfaceIndex')]
        [String[]] $InterfaceIndex,

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

        [Parameter(Mandatory=$false)]
        [Switch] $AdapterStateOnly,

        [Parameter(Mandatory=$false)]
        [Switch] $NodeStateOnly
    )

    $Pass = '+'
    $Fail = '-'
    $testsFailed = 0

#region Get Interfaces
    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 }
    }
    ElseIf ($PSBoundParameters.ContainsKey('InterfaceIndex')) {
        $Interfaces = Get-Interfaces -InterfaceIndex $InterfaceIndex
    }
#endregion Get Interfaces

#region Node State

    #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"
            $toolsInstalled = $true
        }
        else {
            Write-Host "[$Fail] Is Installed: RSAT Data Center Bridging LLDP Tools" -ForegroundColor Red
            $toolsInstalled = $false
            $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"
            $toolsInstalled = $true
        }
        else {
            Write-Host "[$Fail] Is Installed: RSAT-DataCenterBridging-LLDP-Tools" -ForegroundColor Red
            $toolsInstalled = $false
            $testsFailed ++
        }
    }

    Remove-Variable PassFail, isLLDPInstalled -ErrorAction SilentlyContinue

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

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

    if ($isEvtLogEnabled) {
        Write-Host "[$Pass] Is Found: Event Log (Microsoft-Windows-LinkLayerDiscoveryProtocol/Diagnostic)"
        $EvtLogFound = $true
    }
    Else {
        Write-Host "[$Fail] Is Found: Event Log (Microsoft-Windows-LinkLayerDiscoveryProtocol/Diagnostic)" -ForegroundColor Red
        $EvtLogFound = $false
        $testsFailed ++
    }

    if ($isEvtLogEnabled.IsEnabled) {
        Write-Host "[$Pass] Is Enabled: Event Log (Microsoft-Windows-LinkLayerDiscoveryProtocol/Diagnostic)"
        $EvtLogEnabled = $true
    }
    Else {
        Write-Host "[$Fail] Is Enabled: Event Log (Microsoft-Windows-LinkLayerDiscoveryProtocol/Diagnostic)" -ForegroundColor Red
        $EvtLogEnabled = $true
        $testsFailed ++
    }

    if ($isEvtLogEnabled.FileSize -lt ($isEvtLogEnabled.MaximumSizeInBytes * .9)) {
        Write-Host "[$Pass] Is NOT Full: Microsoft-Windows-LinkLayerDiscoveryProtocol/Diagnostic"
        $isNotFull = $true
    }
    Else {
        Write-Host "[$Fail] Is NOT Full: Microsoft-Windows-LinkLayerDiscoveryProtocol/Diagnostic" -ForegroundColor Red
        $isNotFull = $false
        $testsFailed ++
    }
    #endregion

    if ($NodeStateOnly) {
        $NodeState = @{}

        $NodeState = [PSCustomObject] @{
            Node          = $Env:COMPUTERNAME
            Model         = $NodeModel
            LLDPInstalled = $toolsInstalled
            EvtLogFound   = $EvtLogFound
            EvtLogEnabled = $EvtLogEnabled
            EvtLogNotFull    = $isNotFull
        }

        Return $NodeState
    }
#endregion Node State

#region Adapter State
    $remainingIndexes = $Interfaces.ifIndex

    <#
        IndexesMissingEvents is returned from Get-LLDPEvents. We reset before calling the function; any interface index found in this variable
        after the function call did not have an LLDP 10041 packet in the event log.
    #>

    $global:IndexesMissingEvents = $Null

    # doesn't appear to be used...
    $lldpEvent = Get-LLDPEvents -RemainingIndexes $remainingIndexes

    if ($AdapterStateOnly) { $AdapterState = @() }
    foreach ($interface in $Interfaces) {
        if ($interface.Status -eq 'Up') { Write-Host "[$Pass] Is Up: $($interface.Name)" }
        Else { Write-Host "[$Fail] Is Up: $($interface.Name)" -ForegroundColor Red; $testsFailed ++ }

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

        if ($interface.ifIndex -notin $global:IndexesMissingEvents) {
            Write-Host "[$Pass] Is Found: LLDP Packet for index $($interface.Name) [Index $($interface.ifIndex)]"
            $EventExistsforIndex = $true

            Remove-Variable PassFail -ErrorAction SilentlyContinue
        }
        Else {
            Write-Host "[$Fail] Is Found: LLDP Packet for index $($interface.Name) [Index $($interface.ifIndex)]" -ForegroundColor Red
            $EventExistsforIndex = $false

            $testsFailed ++
        }

        if ($AdapterStateOnly) {
            $thisAdapterState = @{}

            $thisAdapterState = [PSCustomObject] @{
                Name            = $interface.Name
                Status          = $interface.Status
                MediaType       = $interface.MediaType
                LLDPEventExists = $EventExistsforIndex
            }

            $AdapterState += $thisAdapterState
        }
    }

    if ($AdapterStateOnly) { return $AdapterState }

#endregion Adapter State

    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 = 'InterfaceIndex')]
        [UInt32[]] $InterfaceIndex,

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

    # run LBFO testing. Used to throw a warning and nothing else.
    if ($PSBoundParameters.ContainsKey('SwitchName'))
    {
        $null = Test-LbfoEnabled -SwitchName $SwitchName
    }
    elseif ($PSBoundParameters.ContainsKey('InterfaceIndex')) 
    {
        $null = Test-LbfoEnabled -InterfaceIndex $InterfaceIndex
    }
    elseif ($PSBoundParameters.ContainsKey('InterfaceNames')) 
    {
        $null = Test-LbfoEnabled -InterfaceNames $InterfaceNames
    }

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

    if ($computerInfo.CsModel -eq 'Virtual Machine') { return (Write-Error 'Cannot be enabled on a virtual machine.' -EA Stop) }

    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.InstallState -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 }
    }
    ElseIf ($PSBoundParameters.ContainsKey('InterfaceIndex')) {
        $Interfaces = Get-Interfaces -InterfaceIndex $InterfaceIndex
    }

    $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
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ParameterSetName = 'InterfaceNames', Position=0)]
        [String[]]
        $InterfaceNames,

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

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


    if ($PSBoundParameters.ContainsKey('SwitchName'))
    {
        $lbfoEnabled = Test-LbfoEnabled -SwitchName $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
        {
            # jakehr: using {return (Write-Error -EA Stop)} will send a terminating error back to the calling program and ends function execution. Good for automation.
            return (Write-Error "'$SwitchName' is not a Switch Embedded Team" -EA Stop)
        }
    }
    elseif ($PSBoundParameters.ContainsKey('InterfaceNames'))
    {
        $lbfoEnabled = Test-LbfoEnabled -InterfaceNames $InterfaceNames

        [array]$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
        #
        # jakehr: typecast the variable as [array] to fix the funkiness.

        $AdapterCount = $NetAdapters.Count

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

            #break <<<<< jakehr: implies we always leave Get-FabricInfo in this code path, so converting this to a terminating error return
            return (Write-Error "Interface enumeration failure. $enumError" -EA Stop)
        }
        else
        {
            [array]$Interfaces = Get-Interfaces -InterfaceNames $InterfaceNames
        }
    }
    elseIf ($PSBoundParameters.ContainsKey('InterfaceIndex'))
    {
        $lbfoEnabled = Test-LbfoEnabled -InterfaceIndex $InterfaceIndex

        [array]$Interfaces = Get-Interfaces -InterfaceIndex $InterfaceIndex

        if (-NOT $Interfaces)
        {
            return (Write-Error "Interface enumeration failure. The interface index(es) were not found: $($InterfaceIndex -join ", ")" -EA Stop)
        }
    }

    # Going to try and find a SET switch. This is not a terminating error at this point.
    if (-NOT $SwitchName)
    {
        
        $VMSwitchTeam = Get-VMSwitchTeam -EA SilentlyContinue

        if ($VMSwitchTeam)
        {
            $SwitchName = $VMSwitchTeam | Where-Object { $_.NetAdapterInterfaceDescription[0] -in ($Interfaces.InterfaceDescription) } | ForEach-Object Name
        }
    }

    [int[]]$remainingIndexes = $Interfaces.ifIndex

    $lldpEvent = Get-LLDPEvents -RemainingIndexes $remainingIndexes

    if ($lldpEvent.count -ne $remainingIndexes.Count)
    {
        # jakehr: convert to warning text from scary red text
        Write-Warning "Could not find an LLDP Packet one or more of the interfaces specified. Please run Test-FabricInfo."
    }
    else
    {
        $InterfaceTable = Parse-LLDPPacket -Events $lldpEvent
    }

    #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 = @()

    # force array since there may only be a single vNIC team map, or a single NIC "team"
    [array]$HostVNICTeamMap = Get-VMNetworkAdapterTeamMapping -ManagementOS | Where-Object NetAdapterName -in $Interfaces.Name

    foreach ($thisInterface in $Interfaces)
    {
        #$thisInterface = $_
        $InterfaceBinding = Get-NetAdapterBinding -Name $thisInterface.Name -ComponentID ms_tcpip, vms_pp

        if (($InterfaceBinding | Where-Object ComponentID -eq 'vms_pp').Enabled -eq $true )
        {
            if ($HostVNICTeamMap)
            {
                try
                {
                    $thisHostVNICParentAdapter = ($HostVNICTeamMap | Where-Object NetAdapterName -eq $thisInterface.Name).ParentAdapter
                    [array]$HostNetAdapterWithIP = Get-NetAdapter -Name $thisHostVNICParentAdapter.Name
                }
                catch
                {
                    Write-Verbose "This interface ($($thisInterface.Name)) does not have a team mapping. Defaulting to all host vNICs."
                }
            }
        }
        else
        {
            [array]$HostNetAdapterWithIP = $thisInterface
        }

        # This handles situations where there are no mappings between pNIC and host vNIC, which means no host vNICs were discovered
        # In this scenario we can assume $thisInterface is a pNIC without a map or there is no SET switch.
        if (-NOT $HostNetAdapterWithIP -and $PSBoundParameters.ContainsKey('SwitchName'))
        {
            $thisHostVNICParentAdapter = $thisInterface

            # since any host net adapter can use this interface we make HostNetAdapterWithIP equal to all host vNICs on the SET switch
            # match on mac address since VMNetworkAdapter and NetAdapter names may not be equal, and exclude the active pNIC
            [array]$HostvNicMacs = Get-VMNetworkAdapter -ManagementOS -SwitchName $SwitchName -EA SilentlyContinue | ForEach-Object { $_.MacAddress -replace '..(?!$)', '$&-' }
            $HostNetAdapterWithIP = Get-NetAdapter | Where-Object { $_.MacAddress -in $HostvNicMacs -and $_.InterfaceDescription -match "Hyper-V" }
        }

        foreach ($thisHostNetAdapter in $HostNetAdapterWithIP)
        {
            #$thisHostNetAdapter = $_
            $thisIP = Get-NetIPAddress -InterfaceIndex $thisHostNetAdapter.ifIndex -AddressFamily IPv4 -EA SilentlyContinue

            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
            }
        }

        Remove-Variable HostNetAdapterWithIP -EA SilentlyContinue
    }

    $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 = 'InterfaceIndex')]
        [String[]] $InterfaceIndex,

        [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 }
    }
    ElseIf ($PSBoundParameters.ContainsKey('InterfaceIndex')) {
        $Interfaces = Get-Interfaces -InterfaceIndex $InterfaceIndex
    }
    #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