AzStackHciNetwork/AzStackHci.Network.Helpers.psm1

Import-LocalizedData -BindingVariable lnTxt -FileName AzStackHci.Network.Strings.psd1

function Test-MgmtIpRange
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, HelpMessage = "Specify starting Management IP Range")]
        [System.Net.IPAddress]
        $StartingAddress,

        [Parameter(Mandatory = $false, HelpMessage = "Specify end Management IP Range")]
        [System.Net.IPAddress]
        $EndingAddress,

        [int[]]
        $port = @(5986, 5985, 22),

        [int]
        $Timeout = 1000,

        [int]
        $Minimum = 5,

        [int]
        $Maximum = 255
    )
    try
    {
        $instanceResults = @()
        # Check same subnet
        $TestMgmtSubnet = TestMgmtSubnet -StartingAddress $StartingAddress -EndingAddress $EndingAddress
        $Status = if ($TestMgmtSubnet) { 'SUCCESS' } else { 'FAILURE' }
        $params = @{
            Name               = 'AzStackHci_Network_Test_Management_IP_Range_Subnet'
            Title              = 'Test Management IP Subnet'
            DisplayName        = "Test Management IP Subnet $StartingAddress - $EndingAddress"
            Severity           = 'CRITICAL'
            Description        = 'Checking start and end address are on the same subnet'
            Tags               = @{}
            Remediation        = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites#network-requirements'
            TargetResourceID   = "$StartingAddress-$EndingAddress"
            TargetResourceName = "ManagementIPRange"
            TargetResourceType = 'Network Range'
            Timestamp          = [datetime]::UtcNow
            Status             = $Status
            AdditionalData     = @{
                Source    = 'CustomerNetwork'
                Resource  = 'CustomerSubnet'
                Detail    = if ($TestMgmtSubnet) { $lnTxt.TestMgmtSubnetPass -f $StartingAddress, $EndingAddress } else { $lnTxt.TestMgmtSubnetFail -f $StartingAddress, $EndingAddress }
                Status    = $status
                TimeStamp = [datetime]::UtcNow
            }
            HealthCheckSource  = $ENV:EnvChkrId
        }
        $instanceResults += New-AzStackHciResultObject @params

        # Check range size
        $TestMgmtRangeSize = TestMgmtRangeSize -StartingAddress $StartingAddress -EndingAddress $EndingAddress -Minimum $Minimum -Maximum $Maximum
        $status = if ($TestMgmtRangeSize) { 'SUCCESS' } else { 'FAILURE' }
        $params = @{
            Name               = 'AzStackHci_Network_Test_Management_IP_Range_Size'
            Title              = 'Test Management IP Range Size'
            DisplayName        = "Test Management IP Range Size $StartingAddress - $EndingAddress"
            Severity           = 'CRITICAL'
            Description        = "Checking management IP range size is between $minimum-$maximum"
            Tags               = @{}
            Remediation        = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites#network-requirements'
            TargetResourceID   = "$StartingAddress-$EndingAddress"
            TargetResourceName = "ManagementIPRange"
            TargetResourceType = 'Network Range'
            Timestamp          = [datetime]::UtcNow
            Status             = $Status
            AdditionalData     = @{
                Source    = 'CustomerNetwork'
                Resource  = 'CustomerRange'
                Detail    = if ($TestMgmtRangeSize) { $lnTxt.TestMgmtRangeSizePass -f $Minimum, $Maximum } else { $lnTxt.TestMgmtRangeSizeFail -f $Minimum, $Maximum }
                Status    = if ($TestMgmtRangeSize) { 'SUCCESS' } else { 'FAILURE' }
                TimeStamp = [datetime]::UtcNow
            }
            HealthCheckSource  = $ENV:EnvChkrId
        }
        $instanceResults += New-AzStackHciResultObject @params

        # Get IP in Range
        $MgmtIpRange = GetMgmtIpRange -StartingAddress $StartingAddress -EndingAddress $EndingAddress
        foreach ($Ip in $MgmtIpRange)
        {
            $result = @{}
            $result += @{
                'Ping' = Test-NetConnection -ComputerName $Ip -InformationLevel Quiet -WarningAction SilentlyContinue
            }
            foreach ($p in $port)
            {
                $result += @{
                    $p = IsTcpPortInUse -Ip $ip -Port $p -Timeout $Timeout
                }
            }
            $Status = if ($true -notin $result.Values) { 'SUCCESS' } else { 'FAILURE' }
            $msg = $lnTxt.ActiveHostCheck -f $ip, (($result.Keys | ForEach-Object { "{0}:{1}" -f $psitem,$result[$psitem] }) -join ', ')
            $Type = if ($result.Values -contains $true) { 'WARNING' } else { 'INFORMATIONAL' }
            Log-Info $msg -Type $Type

            $params = @{
                Name               = 'AzStackHci_Network_Test_Management_IP_No_Active_Hosts'
                Title              = 'Test Management IP Range for Active Hosts'
                DisplayName        = "Test Management IP Range $Ip for Active Hosts"
                Severity           = 'CRITICAL'
                Description        = 'Checking no hosts respond on Management IP range'
                Tags               = @{}
                Remediation        = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites#network-requirements'
                TargetResourceID   = $Ip
                TargetResourceName = "ManagementIPRange"
                TargetResourceType = 'Network Range'
                Timestamp          = [datetime]::UtcNow
                Status             = $Status
                AdditionalData     = @{
                    Source    = $Ip
                    Resource  = 'ICMP/SSH/WINRM'
                    Detail    = ($result.Keys | ForEach-Object { "{0}:{1}" -f $psitem,$result[$psitem] }) -join ', '
                    Status    = $Status
                    TimeStamp = [datetime]::UtcNow
                }
                HealthCheckSource  = $ENV:EnvChkrId
            }
            $instanceResults += New-AzStackHciResultObject @params
        }
        return $instanceResults
    }
    catch
    {
        throw $_
    }
}

# Run during both Deployment and AddNode
# 1. Mgmt NIC IP should not be overlapping with IP Pool
# 2. Ensure Mgmt NIC IPs and IP Pool are in the same subnet
function TestDHCPStatus
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, HelpMessage = "Specify starting infra IP Range for DHCP")]
        [System.Net.IPAddress]
        $StartingAddress,

        [Parameter(Mandatory = $false, HelpMessage = "Specify end infra IP Range for DHCP")]
        [System.Net.IPAddress]
        $EndingAddress,

        [System.Management.Automation.Runspaces.PSSession[]]
        $PsSession
    )
    try
    {
        $instanceResults = @()
        $AdditionalData = @()
        foreach ($session in $PsSession) {
            $sb = {
                $env:COMPUTERNAME
                (
                    Get-NetIPConfiguration |
                    Where-Object {
                        $_.IPv4DefaultGateway -ne $null -and
                        $_.NetAdapter.Status -eq "Up"
                    }
                ).IPv4Address.IPAddress
            }
            $NewNodeData = Invoke-Command $session -ScriptBlock $sb
            $NodeName = $NewNodeData[0]
            # Check for all of the IPs found on the Host
            for ($i = 1; $i -lt $NewNodeData.count; $i++) {
                $NodeManagementIPAddress = $NewNodeData[$i]

                Log-Info "Node Name retrieved from session: $NodeName"
                Log-Info "Node Management IP Address retrieved from session: $NodeManagementIPAddress"
                # Check node management IP is not in infra pool range
                Log-Info "Starting Test Mgmt IP is not in Infra IP Pool for $($session.ComputerName)"
                $ip = [system.net.ipaddress]::Parse($NodeManagementIPAddress).GetAddressBytes()
                [array]::Reverse($ip)
                $ip = [system.BitConverter]::ToUInt32($ip, 0)

                $from = [system.net.ipaddress]::Parse($StartingAddress).GetAddressBytes()
                [array]::Reverse($from)
                $from = [system.BitConverter]::ToUInt32($from, 0)

                $to = [system.net.ipaddress]::Parse($EndingAddress).GetAddressBytes()
                [array]::Reverse($to)
                $to = [system.BitConverter]::ToUInt32($to, 0)

                $mgmtIPOutsideRange = ($ip -le $from) -or ($ip -ge $to)
                if ($mgmtIPOutsideRange) {
                    $TestMgmtIPInfraRangeDetail = $lnTxt.TestMgmtIPInfraRangePass -f $NodeManagementIPAddress, $StartingAddress, $EndingAddress
                }
                else {
                    $TestMgmtIPInfraRangeDetail = $lnTxt.TestMgmtIPInfraRangeFail -f $NodeManagementIPAddress, $StartingAddress, $EndingAddress
                    Log-Info $TestMgmtIPInfraRangeDetail -Type Warning
                }
                $status = if ($mgmtIPOutsideRange) { 'SUCCESS' } else { 'FAILURE' }

                $params = @{
                    Name               = 'AzStackHci_Network_Test_New_DHCP_Validity_Infra_Pool'
                    Title              = 'Test DHCP Configuration Validity Mgmt IP Infra Pool'
                    DisplayName        = "Test DHCP Configuration Validity Mgmt IP Infra Pool"
                    Severity           = 'CRITICAL'
                    Description        = 'Checking Mgmt IPs are not in Infra IP Pool'
                    Tags               = @{}
                    Remediation        = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites#network-requirements'
                    TargetResourceID   = "$StartingAddress-$EndingAddress"
                    TargetResourceName = "DHCPDeploymentConfiguration"
                    TargetResourceType = 'DHCPConfiguration'
                    Timestamp          = [datetime]::UtcNow
                    Status             = $status
                    AdditionalData     = @{
                        Source    = $session.ComputerName
                        Resource  = 'DHCPNodeManagementIP'
                        Detail    = $TestMgmtIPInfraRangeDetail
                        Status    = $status
                        TimeStamp = [datetime]::UtcNow
                    }
                    HealthCheckSource  = $ENV:EnvChkrId
                }
                $instanceResults += New-AzStackHciResultObject @params

                $TestMgmtSubnet = TestMgmtSubnet -StartingAddress $NodeManagementIPAddress -EndingAddress $EndingAddress
                $status = if ($TestMgmtSubnet) { 'SUCCESS' } else { 'FAILURE' }

                $params = @{
                    Name               = 'AzStackHci_Network_Test_New_DHCP_Validity_Infra_Subnet'
                    Title              = 'Test DHCP Configuration Validity Mgmt IP Infra Subnet'
                    DisplayName        = "Test DHCP Configuration Validity Mgmt IP Infra Subnet"
                    Severity           = 'CRITICAL'
                    Description        = 'Checking Mgmt IPs are in same subnet as infra IP Pool'
                    Tags               = @{}
                    Remediation        = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites#network-requirements'
                    TargetResourceID   = "$StartingAddress-$EndingAddress"
                    TargetResourceName = "DHCPDeploymentConfiguration"
                    TargetResourceType = 'DHCPConfiguration'
                    Timestamp          = [datetime]::UtcNow
                    Status             = $status
                    AdditionalData     = @{
                        Source    = "$($session.ComputerName)AndCustomerNetwork"
                        Resource  = 'DHCPNodeManagementIPAndCustomerSubnet'
                        Detail    = if ($TestMgmtSubnet) { $lnTxt.TestMgmtSubnetPass -f $NodeManagementIPAddress, $EndingAddress } else { $lnTxt.TestMgmtSubnetFail -f $NodeManagementIPAddress, $EndingAddress }
                        Status    = $status
                        TimeStamp = [datetime]::UtcNow
                    }
                    HealthCheckSource  = $ENV:EnvChkrId
                }
                $instanceResults += New-AzStackHciResultObject @params
            }
        }
        return $instanceResults
    }
    catch
    {
        throw $_
    }
}

# Initial tests to determine if Mgmt IP of new Node is OK
# Below Tests are for Static IP Allocation (Non-DHCP)
function TestMgmtIPForNewNode
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, HelpMessage = "Specify starting infra IP Range")]
        [System.Net.IPAddress]
        $StartingAddress,

        [Parameter(Mandatory = $false, HelpMessage = "Specify end infra IP Range")]
        [System.Net.IPAddress]
        $EndingAddress,

        [System.Management.Automation.Runspaces.PSSession]
        $PsSession,

        [Hashtable]
        $NodeToManagementIPMap,

        [String]
        $FirstAdapterName,

        [String]
        $IntentName
    )
    try
    {
        $sb = {
            $env:COMPUTERNAME
            (
                Get-NetIPConfiguration |
                Where-Object {
                    $_.IPv4DefaultGateway -ne $null -and
                    $_.NetAdapter.Status -eq "Up"
                }
            ).IPv4Address.IPAddress
        }
        $NewNodeData = Invoke-Command $PsSession -ScriptBlock $sb
        $NodeName = $NewNodeData[0]
        $NodeManagementIPAddress = $NewNodeData[1]

        Log-Info "Node Name retrieved from PSSession: $NodeName"
        Log-Info "Node Management IP Address retrieved from PSSession: $NodeManagementIPAddress"
        # Check node management IP is not in infra pool range
        Log-Info "Starting Test Mgmt IP is not in Infra IP Pool for $($psSession.ComputerName)"
        $ip = [system.net.ipaddress]::Parse($NodeManagementIPAddress).GetAddressBytes()
        [array]::Reverse($ip)
        $ip = [system.BitConverter]::ToUInt32($ip, 0)

        $from = [system.net.ipaddress]::Parse($StartingAddress).GetAddressBytes()
        [array]::Reverse($from)
        $from = [system.BitConverter]::ToUInt32($from, 0)

        $to = [system.net.ipaddress]::Parse($EndingAddress).GetAddressBytes()
        [array]::Reverse($to)
        $to = [system.BitConverter]::ToUInt32($to, 0)

        $instanceResults = @()
        $AdditionalData = @()
        $mgmtIPOutsideRange = ($ip -le $from) -or ($ip -ge $to)
        if ($mgmtIPOutsideRange) {
            $TestMgmtIPInfraRangeDetail = $lnTxt.TestMgmtIPInfraRangePass -f $NodeManagementIPAddress, $StartingAddress, $EndingAddress
            $status = 'SUCCESS'
        }
        else {
            $TestMgmtIPInfraRangeDetail = $lnTxt.TestMgmtIPInfraRangeFail -f $NodeManagementIPAddress, $StartingAddress, $EndingAddress
            Log-Info $TestMgmtIPInfraRangeDetail -Type Warning
            $status = 'FAILURE'
        }
        $params = @{
            Name               = 'AzStackHci_Network_Test_New_Node_Validity_Outside_Mgmt_Range'
            Title              = 'Test New Node Configuration Outside Management Range'
            DisplayName        = "Test New Node Configuration Outside Management Range"
            Severity           = 'CRITICAL'
            Description        = 'Checking New Node IP'
            Tags               = @{}
            Remediation        = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites#network-requirements'
            TargetResourceID   = $NodeManagementIPAddress
            TargetResourceName = "IPAddress"
            TargetResourceType = 'IPAddress'
            Timestamp          = [datetime]::UtcNow
            Status             = $status
            AdditionalData     = @{
                Source    = $NodeName
                Resource  = 'NewNodeManagementIP'
                Detail    = $TestMgmtIPInfraRangeDetail
                Status    = $status
                TimeStamp = [datetime]::UtcNow
            }
            HealthCheckSource  = $ENV:EnvChkrId
        }
        $instanceResults += New-AzStackHciResultObject @params

        # Check that no management IPs are the same (Mgmt IP shouldn't conflict with existing node)
        Log-Info "Starting Test for No Mgmt IPs are the same for any Nodes"
        $duplicateIPs = $false
        $numDuplicates = $NodeToManagementIPMap.GetEnumerator() | Group-Object Value | ? { $_.Count -gt 1 }
        if ($numDuplicates -ne $null) {
            $duplicateIPs = $true
            Log-Info 'Duplicate IPs found for Node Management IPs' -Type Warning
        }

        if ($duplicateIPs) {
            $dtl = 'Duplicate IPs found for Node Management IPs'
            $status = 'FAILURE'
        }
        else {
            $dtl = 'No Duplicate IPs found for Node Management IPs'
            $status = 'SUCCESS'
        }

        $params = @{
            Name               = 'AzStackHci_Network_Test_New_Node_Validity_Duplicate_IP'
            Title              = 'Test New Node Configuration Duplicate IP'
            DisplayName        = "Test New Node Configuration Duplicate IP"
            Severity           = 'CRITICAL'
            Description        = 'Checking New Node IP is not a duplicate'
            Tags               = @{}
            Remediation        = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites#network-requirements'
            TargetResourceID   = $NodeManagementIPAddress
            TargetResourceName = "IPAddress"
            TargetResourceType = 'IPAddress'
            Timestamp          = [datetime]::UtcNow
            Status             = $status
            AdditionalData     = @{
                Source    = 'NodeAndManagementIPMapping'
                Resource  = 'NodeManagementIPs'
                Detail    = $dtl
                Status    = $status
                TimeStamp = [datetime]::UtcNow
            }
            HealthCheckSource  = $ENV:EnvChkrId
        }
        $instanceResults += New-AzStackHciResultObject @params

        # Check that host name exists, and the name and mgmt IP both match current node
        Log-Info "Starting Test to check if Mgmt IP is on a different node as $NodeName"
        Log-Info "Starting simultaneous Test to check if HostName and Mgmt IP Match for $NodeName"
        $ipOnAnotherNode = $false
        $NodeNameAndIPMatches = $false
        $nodeNameForIP = $null
        foreach ($NodeIP in $NodeToManagementIPMap.GetEnumerator()) {
            Write-Host "$($NodeIP.Name): $($NodeIP.Value)"
            if ($NodeIP.Name -eq $NodeName) {
                if ($NodeIP.Value -eq $NodeManagementIPAddress) {
                    $NodeNameAndIPMatches = $true
                    $nodeNameForIP = $NodeIP.Name
                }
            } else {
                if ($NodeIP.Value -eq $NodeManagementIPAddress) {
                    $ipOnAnotherNode = $true
                    $nodeNameForIP = $NodeIP.Name
                }
            }
        }

        if ($ipOnAnotherNode) {
            $CheckMgmtIPNotOnOtherNodeDetail = $lnTxt.CheckMgmtIPNotOnOtherNodeFail -f $NodeManagementIPAddress, $nodeNameForIP
            Log-Info $CheckMgmtIPNotOnOtherNodeDetail -Type Warning
        }
        else {
            $CheckMgmtIPNotOnOtherNodeDetail = $lnTxt.CheckMgmtIPNotOnOtherNodePass -f $NodeManagementIPAddress, $nodeNameForIP
        }
        $status = if ($ipOnAnotherNode) { 'FAILURE' } else { 'SUCCESS' }
        $params = @{
            Name               = 'AzStackHci_Network_Test_New_Node_Validity_IP_Conflict'
            Title              = 'Test New Node Configuration Conflicting IP'
            DisplayName        = "Test New Node Configuration Conflicting IP"
            Severity           = 'CRITICAL'
            Description        = 'Checking New Node IP is not on another node'
            Tags               = @{}
            Remediation        = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites#network-requirements'
            TargetResourceID   = $NodeManagementIPAddress
            TargetResourceName = "IPAddress"
            TargetResourceType = 'IPAddress'
            Timestamp          = [datetime]::UtcNow
            Status             = $status
            AdditionalData     = @{
                Source    = 'NodeAndManagementIPMapping'
                Resource  = 'NodeNameAndManagementIP'
                Detail    = $CheckMgmtIPNotOnOtherNodeDetail
                Status    = $status
                TimeStamp = [datetime]::UtcNow
            }
            HealthCheckSource  = $ENV:EnvChkrId
        }
        $instanceResults += New-AzStackHciResultObject @params

        if ($NodeNameAndIPMatches) {
            $CheckMgmtIPOnNewNodeDetail = $lnTxt.CheckMgmtIPOnNewNodePass -f $NodeManagementIPAddress, $nodeNameForIP
            $status = 'SUCCESS'
        }
        else {
            $CheckMgmtIPOnNewNodeDetail = $lnTxt.CheckMgmtIPOnNewNodeFail -f $NodeManagementIPAddress, $nodeNameForIP
            Log-Info $CheckMgmtIPOnNewNodeDetail -Type Warning
            $status = 'FAILURE'
        }

        $params = @{
            Name               = 'AzStackHci_Network_Test_New_Node_And_IP_Match'
            Title              = 'Test New Node Configuration Name and IP Match'
            DisplayName        = "Test New Node Configuration Name and IP Match"
            Severity           = 'CRITICAL'
            Description        = 'Checking New Node Name and IP match'
            Tags               = @{}
            Remediation        = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites#network-requirements'
            TargetResourceID   = $NodeManagementIPAddress
            TargetResourceName = "IPAddress"
            TargetResourceType = 'IPAddress'
            Timestamp          = [datetime]::UtcNow
            Status             = $status
            AdditionalData     = @{
                Source    = 'NodeAndManagementIPMapping'
                Resource  = 'NewNodeNameAndManagementIP'
                Detail    = $CheckMgmtIPOnNewNodeDetail
                Status    = $status
                TimeStamp = [datetime]::UtcNow
            }
            HealthCheckSource  = $ENV:EnvChkrId
        }
        $instanceResults += New-AzStackHciResultObject @params

        # Check that New Node has the first physical adapter and the physical adapter has the mgmt IP
        Log-Info "Starting Test to see if $FirstAdapterName on $NodeName has the correct Mgmt IP"
        $adapterSB = {
            param($adapterName)
            $returnDict = @{}
            $returnDict["GetNetIPAddressOutput"] = Get-NetIPAddress
            $returnDict["GetNetAdapterOutput"] = Get-NetAdapter
            $AdapterIPObject = Get-NetIPAddress -InterfaceAlias $adapterName -AddressFamily IPv4 -ErrorAction SilentlyContinue
            if ($AdapterIPObject -eq $null) {
                $returnDict["Result"] = $false
                $returnDict["AdapterName"] = $adapterName
                return $returnDict
            }
            $returnDict["Result"] = $true
            $returnDict["AdapterName"] = $adapterName
            $returnDict["AdapterIP"] = $AdapterIPObject.IPAddress
            return $returnDict
        }

        $AdapterContainsMgmtIP = $false
        $physicalAdapterExists = $false
        $VirtualNICName = "vManagement($IntentName)"
        try {
            $NewNodeAdapterData = Invoke-Command $PsSession -ScriptBlock $adapterSB -ArgumentList $FirstAdapterName
            Log-Info "Data found for New Node Adapter ($FirstAdapterName): $($NewNodeAdapterData | Out-String)"
            if ($NewNodeAdapterData['Result'] -eq $false) {
                Log-Info "Physical Adapter Not Found"
                Log-Info "Get-NetIPAddress output: $($NewNodeAdapterData['GetNetIPAddressOutput'] | Out-String)"
                Log-Info "Get-NetAdapter output: $($NewNodeAdapterData['GetNetAdapterOutput'] | Out-String)"
            }
            elseif ($NewNodeAdapterData['Result'] -eq $true -and $NewNodeAdapterData['AdapterIP'] -eq $NodeManagementIPAddress) {
                Log-Info "Physical Adapter found with Correct IP: $($NewNodeAdapterData['AdapterIP'] | Out-String)"
                $physicalAdapterExists = $true
                $AdapterContainsMgmtIP = $true
                $CheckAdapterContainsIPDetail = $lnTxt.CheckAdapterContainsIPPass -f $FirstAdapterName, $NodeManagementIPAddress
            }
            else {
                Log-Info "Physical Adapter found but with incorrect IP"
                Log-Info "Get-NetIPAddress output: $($NewNodeAdapterData['GetNetIPAddressOutput'] | Out-String)"
                Log-Info "Get-NetAdapter output: $($NewNodeAdapterData['GetNetAdapterOutput'] | Out-String)"
            }

            # In certain cases, new node will be set up with the vNIC instead and need to check that for mgmt IP
            if (!$physicalAdapterExists) {
                Log-Info "Physical Adapter does not exist or mgmt IP is wrong. Checking Virtual Adapter" -Type Warning
                $NewNodeVirtualAdapterData = Invoke-Command $PsSession -ScriptBlock $adapterSB -ArgumentList $VirtualNICName
                Log-Info "Data found for New Node Virtual Adapter ($VirtualNICName): $($NewNodeVirtualAdapterData | Out-String)"
                if ($NewNodeVirtualAdapterData['Result'] -eq $false) {
                    Log-Info "Virtual Adapter Not Found"
                    Log-Info "Get-NetIPAddress output: $($NewNodeVirtualAdapterData['GetNetIPAddressOutput'] | Out-String)"
                    Log-Info "Get-NetAdapter output: $($NewNodeVirtualAdapterData['GetNetAdapterOutput'] | Out-String)"
                }
                elseif ($NewNodeVirtualAdapterData['Result'] -eq $true -and $NewNodeVirtualAdapterData['AdapterIP'] -eq $NodeManagementIPAddress) {
                    Log-Info "Virtual Adapter found with Correct IP: $($NewNodeVirtualAdapterData['AdapterIP'] | Out-String)"
                    $AdapterContainsMgmtIP = $true
                    $CheckAdapterContainsIPDetail = $lnTxt.CheckAdapterContainsIPPass -f $VirtualNICName, $NodeManagementIPAddress
                }
                else {
                    Log-Info "Virtual Adapter found but with incorrect IP"
                    Log-Info "Get-NetIPAddress output: $($NewNodeVirtualAdapterData['GetNetIPAddressOutput'] | Out-String)"
                    Log-Info "Get-NetAdapter output: $($NewNodeVirtualAdapterData['GetNetAdapterOutput'] | Out-String)"
                }
            }
        }
        catch {
            Log-Info "Exception thrown when checking New Node Adapter: $_" -Type Warning
        }

        if (!$AdapterContainsMgmtIP) {
            $CheckAdapterContainsIPDetail = $lnTxt.CheckAdapterContainsIPFail -f $FirstAdapterName, $VirtualNICName, $NodeManagementIPAddress
            Log-Info $CheckAdapterContainsIPDetail -Type Warning
            $status = 'FAILURE'
        }
        else
        {
            $status = 'SUCCESS'
        }

        $params = @{
            Name               = 'AzStackHci_Network_Test_New_Node_First_Adapter_Validity'
            Title              = 'Test New Node Configuration First Network Adapter has Mgmt IP'
            DisplayName        = "Test New Node Configuration First Network Adapter has Mgmt IP"
            Severity           = 'CRITICAL'
            Description        = 'Checking New Node first adapter has mgmt IP'
            Tags               = @{}
            Remediation        = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-checklist'
            TargetResourceID   = $NodeManagementIPAddress
            TargetResourceName = $FirstAdapterName
            TargetResourceType = 'Network Adapter'
            Timestamp          = [datetime]::UtcNow
            Status             = $status
            AdditionalData     = @{
                Source    = 'NewNodeAdapter'
                Resource  = 'NewNodeAdapterIP'
                Detail    = $CheckAdapterContainsIPDetail
                Status    = $status
                TimeStamp = [datetime]::UtcNow
            }
            HealthCheckSource  = $ENV:EnvChkrId
        }
        $instanceResults += New-AzStackHciResultObject @params
        return $instanceResults
    }
    catch
    {
        throw $_
    }
}

function TestMgmtSubnet
{
    <#
    .SYNOPSIS
        Ensure Start and End IPs are on the same subnet.
    #>


    param (
        [Parameter(Mandatory = $false, HelpMessage = "Specify starting Management IP Range")]
        [System.Net.IPAddress]
        $StartingAddress,

        [Parameter(Mandatory = $false, HelpMessage = "Specify end Management IP Range")]
        [System.Net.IPAddress]
        $EndingAddress
    )

    try
    {
        $start = $StartingAddress -replace "\.[0-9]{1,3}$", ""
        $end = $EndingAddress -replace "\.[0-9]{1,3}$", ""

        if ($start -eq $end)
        {
            Log-info "Subnet start: $start and end: $end"
            return $true
        }
        else
        {
            return $false
        }
    }
    catch
    {
        throw "Failed to check subnet. Error: $_"
    }
}

function GetMgmtIpRange
{
    param (
        [Parameter(Mandatory = $false, HelpMessage = "Specify starting Management IP Range")]
        [System.Net.IPAddress]
        $StartingAddress,

        [Parameter(Mandatory = $false, HelpMessage = "Specify end Management IP Range")]
        [System.Net.IPAddress]
        $EndingAddress
    )

    try
    {
        $first3 = $StartingAddress -replace "\.[0-9]{1,3}$", ""
        $start = $StartingAddress -split "\." | Select-Object -Last 1
        $end = $EndingAddress -split "\." | Select-Object -Last 1

        $range = $start..$end | ForEach-Object { ([System.Net.IPAddress]("{0}.{1}" -f $first3, $PSITEM)).IPAddressToString }
        Log-info "Start: $start and end: $end gives range: $($range -join ',')"
        return $range
    }
    catch
    {
        throw "Failed to get Mgmt range. Error: $_"
    }
}

function TestMgmtRangeSize
{
    <#
    .SYNOPSIS
        Ensure IP range is within boundaries.
    #>

    param (
        [Parameter(Mandatory = $false, HelpMessage = "Specify starting Management IP Range")]
        [System.Net.IPAddress]
        $StartingAddress,

        [Parameter(Mandatory = $false, HelpMessage = "Specify end Management IP Range")]
        [System.Net.IPAddress]
        $EndingAddress,

        [int]
        $Minimum = 5,

        [int]
        $Maximum = 16
    )

    try
    {
        $start = $StartingAddress -split "\." | Select-Object -Last 1
        $end = $EndingAddress -split "\." | Select-Object -Last 1
        $hostCount = ($start..$end).count
        Log-info "Start: $start and end: $end gives host count: $hostcount"
        if ($hostCount -gt $Maximum -or $hostCount -lt $Minimum)
        {
            return $false
        }
        else
        {
            return $true
        }
    }
    catch
    {
        throw "Failed to check range size. Error: $_"
    }
}

function IsTcpPortInUse
{
    param(
        [System.Net.IPAddress]
        $Ip,

        [int]
        $Port = 5986,

        [int]
        $Timeout = 500
    )

    try
    {
        $tcpClient = New-Object System.Net.Sockets.TcpClient
        $portOpened = $tcpClient.ConnectAsync($ip, $p).Wait($timeout)
        $tcpClient.Dispose()
        return ($portOpened -contains $true)
    }
    catch
    {
        throw "Failed to check TCP ports. Error: $_"
    }
}

function TestNetworkIntentStatus
{
    <#
    .SYNOPSIS
        This test is run in the AddNode context only.
        This test validates if the intents configured on the existing cluster and the new node to be added are not in errored state.
 
    .DESCRIPTION
        This test performs the following Validations:
        1) Check the ATC Intent status on existing nodes are successfully allocated
        2) Check if NetworkATC service is running on the new node
        3) Check if the existing nodes have storage intent configured in them.
 
    .PARAMETERS
        [System.Management.Automation.Runspaces.PSSession] $PsSession
    #>

    [CmdletBinding()]
    param (
        [System.Management.Automation.Runspaces.PSSession]
        $PsSession
    )

    try
    {
        Log-Info "Checking ATC Intent status on existing nodes and if NetworkATC service is running on the new node."
        $instanceResults = @()
        $AdditionalData = @()

        # Get the names of all nodes with an Up Status
        $activeNodes = (Get-ClusterNode | Where-Object {$_.State -eq "Up"}).Name
        Log-Info "Active nodes: $($activeNodes | Out-String)"

        # Get all intents on the active nodes
        $intents = Get-NetIntentStatus | Where-Object {$activeNodes -contains $_.Host}

        # Checks the intent status on the existing nodes.
        foreach ($intent in $intents)
        {
            $intentHealthy = $true
            if ($intent.ConfigurationStatus -ne "Success" -or $intent.ProvisioningStatus -ne "Completed")
            {
                $intentHealthy = $false
                $TestNetworkIntentStatusDetail = $lnTxt.TestNetworkIntentStatusFail -f $intent.Host, $intent.ConfigurationStatus, $intent.ProvisioningStatus
                Log-Info $TestNetworkIntentStatusDetail -Type Warning
            }
            else
            {
                $intentHealthy = $true
                $TestNetworkIntentStatusDetail = $lnTxt.TestNetworkIntentStatusPass -f $intent.Host, $intent.ConfigurationStatus, $intent.ProvisioningStatus
            }

            $params = @{
                Name               = 'AzStackHci_Network_Test_Network_AddNode_Intent_Status'
                Title              = 'Test Network intent on existing nodes'
                DisplayName        = 'Test Network intent on existing nodes'
                Severity           = 'CRITICAL'
                Description        = 'Checking if Network intent is unhealthy on existing nodes'
                Tags               = @{}
                Remediation        = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-checklist'
                TargetResourceID   = 'NetworkIntent'
                TargetResourceName = 'NetworkIntent'
                TargetResourceType = 'NetworkIntent'
                Timestamp          = [datetime]::UtcNow
                Status             = if ($intentHealthy) { 'SUCCESS' } else { 'FAILURE' }
                AdditionalData     = @{
                    Source    = $intent.Host
                    Resource  = 'AddNodeIntentStatusCheck'
                    Detail    = $TestNetworkIntentStatusDetail
                    Status    = if ($intentHealthy) { 'SUCCESS' } else { 'FAILURE' }
                    TimeStamp = [datetime]::UtcNow
                }
                HealthCheckSource  = $ENV:EnvChkrId
            }
            $instanceResults += New-AzStackHciResultObject @params
        }

        Log-Info "Checking if the storage intent is configured on the existing cluster before add node."

        $storageIntent = $intents | Where-Object {$_.IsStorageIntentSet -eq $true}

        try {
            $source = Get-Cluster
        }
        catch {
            $source = $Env:COMPUTERNAME
            Log-Info "Error getting the cluster, we could be running this test in standalone mode on $($source)"
        }

        if ($null -eq $storageIntent)
        {
            $TestNetworkIntentStatusDetail = $lnTxt.TestStorageIntentNotConfigured -f $source
            Log-Info $TestNetworkIntentStatusDetail -Type Warning
        }
        else
        {
            $TestNetworkIntentStatusDetail = $lnTxt.TestStorageIntentConfigured -f $source
            Log-Info $TestNetworkIntentStatusDetail -Type Success
        }

        $params = @{
            Name               = 'AzStackHci_Network_Test_Network_AddNode_Storage_Intent'
            Title              = 'Test Storage intent on existing nodes'
            DisplayName        = 'Test Storage intent on existing nodes'
            Severity           = 'CRITICAL'
            Description        = 'Check if the storage intent is configured on the existing cluster before add node'
            Tags               = @{}
            Remediation        = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-checklist'
            TargetResourceID   = 'StorageIntent'
            TargetResourceName = 'StorageIntent'
            TargetResourceType = 'StorageIntent'
            Timestamp          = [datetime]::UtcNow
            Status             = if ($null -eq $storageIntent) { 'FAILURE' } else { 'SUCCESS' }
            AdditionalData     = @{
                Source    = $source
                Resource  = 'AddNodeStorageIntentCheck'
                Detail    = $TestNetworkIntentStatusDetail
                Status    = if ($null -eq $storageIntent) { 'FAILURE' } else { 'SUCCESS' }
                TimeStamp = [datetime]::UtcNow
            }
            HealthCheckSource  = $ENV:EnvChkrId
        }
        $instanceResults += New-AzStackHciResultObject @params

        # Check if NetworkATC service is running on the new node
        $sb = {
            $retVal = New-Object psobject -Property @{
                Pass = $true
                Status = [string]::Empty
            }

            $atcFeature = Get-WindowsFeature -Name NetworkATC

            if ($atcFeature.Installstate -eq "Installed")
            {
                $atcService = Get-Service NetworkATC -ErrorAction SilentlyContinue
                $retVal.Status = "Feature Installed Service $($atcService.Status)"
            }
            elseif ($atcFeature.Installstate -eq "Available")
            {
                $retVal.Status = "Feature Available"
            }
            else
            {
                $retVal.Pass = $false
            }

            return $retVal
        }

        $NetworkATCStatus = Invoke-Command $PsSession -ScriptBlock $sb
        $ATCStatusHealthy = $true
        if (!$NetworkATCStatus.Pass)
        {
            # NetworkATC feature not Installed, not Available on the system
            $ATCStatusHealthy = $false
            $TestNetworkATCServiceDetail = $lnTxt.TestNetworkATCFeatureNotInSystem -f $psSession.ComputerName
            Log-Info $TestNetworkATCServiceDetail -Type Warning
        }
        elseif (-not (($NetworkATCStatus.Status -eq 'Feature Installed Service Running') -or ($NetworkATCStatus.Status -eq 'Feature Available')))
        {
            # NetworkATC feature installed but service not 'Running', or feature not available
            $ATCStatusHealthy = $false
            $TestNetworkATCServiceDetail = $lnTxt.TestNetworkATCFeatureServiceStatus -f $NetworkATCStatus.Status, $psSession.ComputerName
            Log-Info $TestNetworkATCServiceDetail -Type Warning
        }
        else
        {
            $ATCStatusHealthy = $true
            $TestNetworkATCServiceDetail = $lnTxt.TestNetworkATCFeatureServiceStatus -f $NetworkATCStatus.Status, $psSession.ComputerName
            Log-Info $TestNetworkATCServiceDetail -Type Success
        }

        $params = @{
            Name               = 'AzStackHci_Network_Test_Network_AddNode_NetworkATC_Service'
            Title              = 'Test NetworkATC service is running on new node'
            DisplayName        = 'Test NetworkATC service is running on new node'
            Severity           = 'CRITICAL'
            Description        = 'Check NetworkATC service is running on new node'
            Tags               = @{}
            Remediation        = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-checklist'
            TargetResourceID   = 'NetworkATCService'
            TargetResourceName = 'NetworkATCService'
            TargetResourceType = 'NetworkATCService'
            Timestamp          = [datetime]::UtcNow
            Status             = if ($ATCStatusHealthy) { 'SUCCESS' } else { 'FAILURE' }
            AdditionalData     = @{
                Source    = $PsSession.ComputerName
                Resource  = 'AddNodeNewNodeNetworkATCServiceCheck'
                Detail    = $TestNetworkATCServiceDetail
                Status    = if ($ATCStatusHealthy) { 'SUCCESS' } else { 'FAILURE' }
                TimeStamp = [datetime]::UtcNow
            }
            HealthCheckSource  = $ENV:EnvChkrId
        }
        $instanceResults += New-AzStackHciResultObject @params
        return $instanceResults
    }
    catch
    {
        throw $_
    }
}
# SIG # Begin signature block
# MIInwgYJKoZIhvcNAQcCoIInszCCJ68CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCHWuADl/1ZVFa3
# vPAMCKnnG4aQgIo98fPpCajZf30T66CCDXYwggX0MIID3KADAgECAhMzAAADrzBA
# DkyjTQVBAAAAAAOvMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMxMTE2MTkwOTAwWhcNMjQxMTE0MTkwOTAwWjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDOS8s1ra6f0YGtg0OhEaQa/t3Q+q1MEHhWJhqQVuO5amYXQpy8MDPNoJYk+FWA
# hePP5LxwcSge5aen+f5Q6WNPd6EDxGzotvVpNi5ve0H97S3F7C/axDfKxyNh21MG
# 0W8Sb0vxi/vorcLHOL9i+t2D6yvvDzLlEefUCbQV/zGCBjXGlYJcUj6RAzXyeNAN
# xSpKXAGd7Fh+ocGHPPphcD9LQTOJgG7Y7aYztHqBLJiQQ4eAgZNU4ac6+8LnEGAL
# go1ydC5BJEuJQjYKbNTy959HrKSu7LO3Ws0w8jw6pYdC1IMpdTkk2puTgY2PDNzB
# tLM4evG7FYer3WX+8t1UMYNTAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQURxxxNPIEPGSO8kqz+bgCAQWGXsEw
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMTgyNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAISxFt/zR2frTFPB45Yd
# mhZpB2nNJoOoi+qlgcTlnO4QwlYN1w/vYwbDy/oFJolD5r6FMJd0RGcgEM8q9TgQ
# 2OC7gQEmhweVJ7yuKJlQBH7P7Pg5RiqgV3cSonJ+OM4kFHbP3gPLiyzssSQdRuPY
# 1mIWoGg9i7Y4ZC8ST7WhpSyc0pns2XsUe1XsIjaUcGu7zd7gg97eCUiLRdVklPmp
# XobH9CEAWakRUGNICYN2AgjhRTC4j3KJfqMkU04R6Toyh4/Toswm1uoDcGr5laYn
# TfcX3u5WnJqJLhuPe8Uj9kGAOcyo0O1mNwDa+LhFEzB6CB32+wfJMumfr6degvLT
# e8x55urQLeTjimBQgS49BSUkhFN7ois3cZyNpnrMca5AZaC7pLI72vuqSsSlLalG
# OcZmPHZGYJqZ0BacN274OZ80Q8B11iNokns9Od348bMb5Z4fihxaBWebl8kWEi2O
# PvQImOAeq3nt7UWJBzJYLAGEpfasaA3ZQgIcEXdD+uwo6ymMzDY6UamFOfYqYWXk
# ntxDGu7ngD2ugKUuccYKJJRiiz+LAUcj90BVcSHRLQop9N8zoALr/1sJuwPrVAtx
# HNEgSW+AKBqIxYWM4Ev32l6agSUAezLMbq5f3d8x9qzT031jMDT+sUAoCw0M5wVt
# CUQcqINPuYjbS1WgJyZIiEkBMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg
# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03
# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr
# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg
# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy
# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9
# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh
# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k
# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB
# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn
# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90
# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w
# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o
# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa
# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG
# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV
# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG
# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl
# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb
# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l
# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6
# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0
# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560
# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam
# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa
# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah
# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA
# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt
# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr
# /Xmfwb1tbWrJUnMTDXpQzTGCGaIwghmeAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAAOvMEAOTKNNBUEAAAAAA68wDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIMx9sPO0gfeDDw7zD4Nz5eF5
# d6ofP2GnENVm/UtG5bN2MEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAqMFb35zjupmf+0btyRp61rfaR9pDzgcJkSyiXeJwHSx2sTG69ZSB754d
# J+Ic3reXY3Pskdy3iGixIh4VEe/3VJFn5gEwvxP6we1o4Fiac0+NwyV/YZ1GIllv
# LLQDKfjwAsFVTy1/h7pWom56UhY3f95GynBJl79UQ5MS4INKwjC2Xj2uc09Kbo4C
# m8qp0nOFUTCScacfZqKvjhHf9KZbzAhe1UnFkMhzphKYVvXKBBFnM3kk8edDDWPJ
# yFFocqkBout5ACV3lninzCgO291rK0TENVPKyUWJb2k/Jx3Y7JRA24Hb1PS58ZPV
# oRAyE6PqcADCsENZH43fuc+a61Vkl6GCFywwghcoBgorBgEEAYI3AwMBMYIXGDCC
# FxQGCSqGSIb3DQEHAqCCFwUwghcBAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFZBgsq
# hkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCAZwFw6zxRMdSz1RuSj8e6edKfRGfRJZqdtq0KcNJ4LqgIGZbqeuloi
# GBMyMDI0MDIxMjE0MDY1MS45MzdaMASAAgH0oIHYpIHVMIHSMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl
# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNO
# OjA4NDItNEJFNi1DMjlBMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT
# ZXJ2aWNloIIRezCCBycwggUPoAMCAQICEzMAAAHajtXJWgDREbEAAQAAAdowDQYJ
# KoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcNMjMx
# MDEyMTkwNjU5WhcNMjUwMTEwMTkwNjU5WjCB0jELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3Bl
# cmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjowODQyLTRC
# RTYtQzI5QTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJOQBgh2tVFR1j8jQA4NDf8b
# cVrXSN080CNKPSQo7S57sCnPU0FKF47w2L6qHtwm4EnClF2cruXFp/l7PpMQg25E
# 7X8xDmvxr8BBE6iASAPCfrTebuvAsZWcJYhy7prgCuBf7OidXpgsW1y8p6Vs7sD2
# aup/0uveYxeXlKtsPjMCplHkk0ba+HgLho0J68Kdji3DM2K59wHy9xrtsYK+X9er
# bDGZ2mmX3765aS5Q7/ugDxMVgzyj80yJn6ULnknD9i4kUQxVhqV1dc/DF6UBeuzf
# ukkMed7trzUEZMRyla7qhvwUeQlgzCQhpZjz+zsQgpXlPczvGd0iqr7lACwfVGog
# 5plIzdExvt1TA8Jmef819aTKwH1IVEIwYLA6uvS8kRdA6RxvMcb//ulNjIuGceyy
# kMAXEynVrLG9VvK4rfrCsGL3j30Lmidug+owrcCjQagYmrGk1hBykXilo9YB8Qyy
# 5Q1KhGuH65V3zFy8a0kwbKBRs8VR4HtoPYw9z1DdcJfZBO2dhzX3yAMipCGm6Smv
# mvavRsXhy805jiApDyN+s0/b7os2z8iRWGJk6M9uuT2493gFV/9JLGg5YJJCJXI+
# yxkO/OXnZJsuGt0+zWLdHS4XIXBG17oPu5KsFfRTHREloR2dI6GwaaxIyDySHYOt
# vIydla7u4lfnfCjY/qKTAgMBAAGjggFJMIIBRTAdBgNVHQ4EFgQUoXyNyVE9ZhOV
# izEUVwhNgL8PX0UwHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXwYD
# VR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j
# cmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3JsMGwG
# CCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIw
# MjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcD
# CDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggIBALmDVdTtuI0jAEt4
# 1O2OM8CU237TGMyhrGr7FzKCEFaXxtoqk/IObQriq1caHVh2vyuQ24nz3TdOBv7r
# cs/qnPjOxnXFLyZPeaWLsNuARVmUViyVYXjXYB5DwzaWZgScY8GKL7yGjyWrh78W
# JUgh7rE1+5VD5h0/6rs9dBRqAzI9fhZz7spsjt8vnx50WExbBSSH7rfabHendpeq
# bTmW/RfcaT+GFIsT+g2ej7wRKIq/QhnsoF8mpFNPHV1q/WK/rF/ChovkhJMDvlqt
# ETWi97GolOSKamZC9bYgcPKfz28ed25WJy10VtQ9P5+C/2dOfDaz1RmeOb27Kbeg
# ha0SfPcriTfORVvqPDSa3n9N7dhTY7+49I8evoad9hdZ8CfIOPftwt3xTX2RhMZJ
# CVoFlabHcvfb84raFM6cz5EYk+x1aVEiXtgK6R0xn1wjMXHf0AWlSjqRkzvSnRKz
# FsZwEl74VahlKVhI+Ci9RT9+6Gc0xWzJ7zQIUFE3Jiix5+7KL8ArHfBY9UFLz4sn
# boJ7Qip3IADbkU4ZL0iQ8j8Ixra7aSYfToUefmct3dM69ff4Eeh2Kh9NsKiiph58
# 9Ap/xS1jESlrfjL/g/ZboaS5d9a2fA598mubDvLD5x5PP37700vm/Y+PIhmp2fTv
# uS2sndeZBmyTqcUNHRNmCk+njV3nMIIHcTCCBVmgAwIBAgITMwAAABXF52ueAptJ
# mQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNh
# dGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1
# WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEB
# BQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O1YLT/e6cBwfSqWxOdcjK
# NVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWNE893MsAQGOhg
# fWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8OWECesSq/XJp
# rx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxqD89d9P6OU8/W7IVWTe/d
# vI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6BVWYbWg7mka9
# 7aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSWrAFKu75xqRdbZ2De+JKR
# Hh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv231fgLrbqn427DZM9itu
# qBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XYcz1DTsEzOUyO
# ArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYctenIPDC+hIK12NvDMk2ZItb
# oKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6
# bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17aj54WcmnGrnu3tz5q4i6t
# AgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQW
# BBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacb
# UzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYz
# aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnku
# aHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIA
# QwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2
# VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwu
# bWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEw
# LTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93
# d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYt
# MjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEkW+Geckv8qW/q
# XBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6
# U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74py27YP0h1AdkY3m2CDPVt
# I1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1AoL8ZthISEV09J+BAljis
# 9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTp
# kbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0
# sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMBV0lUZNlz138e
# W0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJ
# sWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7
# Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0
# dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lwY1NNje6CbaUFEMFxBmoQ
# tB1VM1izoXBm8qGCAtcwggJAAgEBMIIBAKGB2KSB1TCB0jELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxh
# bmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjow
# ODQyLTRCRTYtQzI5QTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
# dmljZaIjCgEBMAcGBSsOAwIaAxUAQqIfIYljHUbNoY0/wjhXRn/sSA2ggYMwgYCk
# fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF
# AOl0RNYwIhgPMjAyNDAyMTIxNTE4NDZaGA8yMDI0MDIxMzE1MTg0NlowdzA9Bgor
# BgEEAYRZCgQBMS8wLTAKAgUA6XRE1gIBADAKAgEAAgIBRwIB/zAHAgEAAgISkjAK
# AgUA6XWWVgIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIB
# AAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAEHMH8eDwQ5UEvYI
# N/9Kq6PkMHM1J4YpUps7G5vN9ACEiNOyRzo1u51vINRVtW35RkXd/FwSJxwy2FsB
# v/D0WmPV4Zm4NoWqDnB/Dpb4oBYpgmXmXTcFHbPKZldyI3/XMMJxnSLiJ6zmzbCt
# mhPIujUMoFKQpSrn7wOflyWjp5aaMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTACEzMAAAHajtXJWgDREbEAAQAAAdowDQYJYIZIAWUD
# BAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0B
# CQQxIgQgBLjaUWDGsyUXn6q8bnunn3ZTauqlZbBlptN3guTZSvQwgfoGCyqGSIb3
# DQEJEAIvMYHqMIHnMIHkMIG9BCAipaNpYsDvnqTe95Dj1C09020I5ljibrW/ndIC
# Oxg9xjCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u
# MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp
# b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB
# 2o7VyVoA0RGxAAEAAAHaMCIEIDnRr0iTxo7N39RYkJQjNKgSUzMlqo106/A0RY2l
# 63UJMA0GCSqGSIb3DQEBCwUABIICAJBbrHUsdYpj2ppZVxBACRRi93cx8iJpleI7
# DLEWdJPvGgZHfOCOKp4DAf6r/acJm2FjVNzeoW8VWt0ujI2HVjFHiTAOkDrOkEkQ
# Axv5iHwOPb0iJIQN2bseREz3mHUFdQVYqMFF55le3Px68k2KWZuu/njVLJP7MojE
# u2AdFocOkXVPuaNpvt4sLGSQu/PC8FYoBTttW1xf1Y1yBPQVTZAaVEosMzNyo3GU
# y7f1d06ZmxXIluiQZvU4yuQJoPNXOxAw7LGPRdKFNRNIISyFVr2ksHrwaCMksPWb
# zwpa2UQT37vq0GIFNdB6qRMWrbx9x7UKS/nge7Vg7izrrLZ0o9L8k2RW1+lkHZm3
# XUXmlsuMnsYfAx3JChUrJYwTAsyp0KCggvTAzKKMOvwZZxI+94hDF680NtzgXXsN
# IWngqNjh4e/KoC5FHz3MrEmZPxx18SJHRvgsbP9chGPMxw/EqLrAtndind0tH1I7
# IL+II1Rm7ZkuQoOQrTo3gTo5CmKtNZq4furFx8l1jRbdtt37Zti6tltxyvVGMNgw
# on7wF+9evk5o4VZiTqbo3Lt3+JyOowQ+KqXJS4kLbrxD0DjaCBPrr6wAYFaElsGu
# HXYpBryIZunWnu7g5fD14BlLM3c7HPlHi9Zcrk1lVn8D8zyTzXVwJ9zNHj3xIKlB
# kYs3dwdb
# SIG # End signature block