AzStackHciNetwork/AzStackHci.Network.Helpers.psm1

class HealthModel
{
    # Attributes for Azure Monitor schema
    [string]$Name #Name of the individual test/rule/alert that was executed. Unique, not exposed to the customer.
    [string]$Title #User-facing name; one or more sentences indicating the direct issue.
    [string]$Severity #Severity of the result (Critical, Warning, Informational, Hidden) – this answers how important the result is. Critical is the only update-blocking severity.
    [string]$Description #Detailed overview of the issue and what impact the issue has on the stamp.
    [psobject]$Tags #Key-value pairs that allow grouping/filtering individual tests. For example, "Group": "ReadinessChecks", "UpdateType": "ClusterAware"
    [string]$Status #The status of the check running (i.e. Failed, Succeeded, In Progress) – this answers whether the check ran, and passed or failed.
    [string]$Remediation #Set of steps that can be taken to resolve the issue found.
    [string]$TargetResourceID #The unique identifier for the affected resource (such as a node or drive).
    [string]$TargetResourceName #The name of the affected resource.
    [string]$TargetResourceType #The type of resource being referred to (well-known set of nouns in infrastructure, aligning with Monitoring).
    [datetime]$Timestamp #The Time in which the HealthCheck was called.
    [psobject[]]$AdditionalData #Property bag of key value pairs for additional information.
    [string]$HealthCheckSource #The name of the services called for the HealthCheck (I.E. Test-AzureStack, Test-Cluster).
}

class AzStackHciNetworkTarget : HealthModel {}

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
    {

        # Check same subnet
        $AdditionalData = @()
        $TestMgmtSubnet = TestMgmtSubnet -StartingAddress $StartingAddress -EndingAddress $EndingAddress
        $AdditionalData += New-Object -TypeName PSObject -Property @{
            Source    = 'CustomerNetwork'
            Resource  = 'CustomerSubnet'
            Detail    = if ($TestMgmtSubnet) { $lnTxt.TestMgmtSubnetPass -f $StartingAddress, $EndingAddress } else { $lnTxt.TestMgmtSubnetFail -f $StartingAddress, $EndingAddress }
            Status    = if ($TestMgmtSubnet) { 'Succeeded' } else { 'Failed' }
            TimeStamp = [datetime]::UtcNow
        }

        # Check range size
        $TestMgmtRangeSize = TestMgmtRangeSize -StartingAddress $StartingAddress -EndingAddress $EndingAddress -Minimum $Minimum -Maximum $Maximum
        $AdditionalData += New-Object -TypeName PSObject -Property @{
            Source    = 'CustomerNetwork'
            Resource  = 'CustomerRange'
            Detail    = if ($TestMgmtRangeSize) { $lnTxt.TestMgmtRangeSizePass -f $Minimum, $Maximum } else { $lnTxt.TestMgmtRangeSizeFail -f $Minimum, $Maximum }
            Status    = if ($TestMgmtRangeSize) { 'Succeeded' } else { 'Failed' }
            TimeStamp = [datetime]::UtcNow
        }

        # 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
                }
            }
            $AdditionalData += New-Object -TypeName PSObject -Property @{
                Source    = $Ip
                Resource  = 'ICMP/SSH/WINRM'
                Detail    = ($result.Keys | ForEach-Object { "{0}:{1}" -f $psitem,$result[$psitem] }) -join ', '
                Status    = if ($result.Values -contains $true) { 'Failed' } else { 'Succeeded' }
                TimeStamp = [datetime]::UtcNow
            }

            $msg = $lnTxt.ActiveHostCheck -f $ip, (($result.Keys | ForEach-Object { "{0}:{1}" -f $psitem,$result[$psitem] }) -join ', ')
            $Type = if ($result.Values -contains $true) { 'Warning' } else { 'Info' }
            Log-Info $msg -Type $Type
        }

        $instanceResult = New-Object AzStackHciNetworkTarget
        $instanceResult.Name = 'AzStackHci_Network_Test_Management_IP_Range'
        $instanceResult.Title = 'Test Management IP Range'
        $instanceResult.Severity = 'Critical'
        $instanceResult.Description = 'Checking no hosts respond on IP range'
        $instanceResult.Remediation = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-prerequisites#network-requirements'
        $instanceResult.TargetResourceID = 'ManagementIPRange'
        $instanceResult.TargetResourceName = 'ManagementIPRange'
        $instanceResult.TargetResourceType = 'Network Range'
        $instanceResult.Timestamp = [datetime]::UtcNow
        $instanceResult.HealthCheckSource = $ENV:EnvChkrId
        $instanceResult.Status = if ($AdditionalData.Status -contains 'Failed') { 'Failed' } else { 'Succeeded' }
        $instanceResult.AdditionalData = $AdditionalData
        return $instanceResult
    }
    catch
    {
        throw $_
    }
}

# Initial tests to determine if Mgmt IP of new Node is OK
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 -ne "Disconnected"
                }
            ).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)

        $AdditionalData = @()
        $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
        }
        $AdditionalData += New-Object -TypeName PSObject -Property @{
            Source    = $PsSession.ComputerName
            Resource  = 'NewNodeManagementIP'
            Detail    = $TestMgmtIPInfraRangeDetail
            Status    = if ($mgmtIPOutsideRange) { 'Succeeded' } else { 'Failed' }
            TimeStamp = [datetime]::UtcNow
        }

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

        $AdditionalData += New-Object -TypeName PSObject -Property @{
            Source    = 'NodeAndManagementIPMapping'
            Resource  = 'NodeManagementIPs'
            Detail    =
                if ($duplicateIPs) {
                    'Duplicate IPs found for Node Management IPs'
                }
                else {
                    'No Duplicate IPs found for Node Management IPs'
                }
            Status    = if ($duplicateIPs) { 'Failed' } else { 'Succeeded' }
            TimeStamp = [datetime]::UtcNow
        }

        # 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
        }
        $AdditionalData += New-Object -TypeName PSObject -Property @{
            Source    = 'NodeAndManagementIPMapping'
            Resource  = 'NodeNameAndManagementIP'
            Detail    = $CheckMgmtIPNotOnOtherNodeDetail
            Status    = if ($ipOnAnotherNode) { 'Failed' } else { 'Succeeded' }
            TimeStamp = [datetime]::UtcNow
        }

        if ($NodeNameAndIPMatches) {
            $CheckMgmtIPOnNewNodeDetail = $lnTxt.CheckMgmtIPOnNewNodePass -f $NodeManagementIPAddress, $nodeNameForIP
        }
        else {
            $CheckMgmtIPOnNewNodeDetail = $lnTxt.CheckMgmtIPOnNewNodeFail -f $NodeManagementIPAddress, $nodeNameForIP
            Log-Info $CheckMgmtIPOnNewNodeDetail -Type Warning
        }
        $AdditionalData += New-Object -TypeName PSObject -Property @{
            Source    = 'NodeAndManagementIPMapping'
            Resource  = 'NewNodeNameAndManagementIP'
            Detail    = $CheckMgmtIPOnNewNodeDetail
            Status    = if ($NodeNameAndIPMatches) { 'Succeeded' } else { 'Failed' }
            TimeStamp = [datetime]::UtcNow
        }

        # 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
        }
        $AdditionalData += New-Object -TypeName PSObject -Property @{
            Source    = 'NewNodeAdapter'
            Resource  = 'NewNodeAdapterIP'
            Detail    = $CheckAdapterContainsIPDetail
            Status    = if ($AdapterContainsMgmtIP) { 'Succeeded' } else { 'Failed' }
            TimeStamp = [datetime]::UtcNow
        }

        $instanceResult = New-Object AzStackHciNetworkTarget
        $instanceResult.Name = 'AzStackHci_Network_Test_New_Node_Validity'
        $instanceResult.Title = 'Test New Node Configuration Validity'
        $instanceResult.Severity = 'Critical'
        $instanceResult.Description = 'Checking New Node values'
        $instanceResult.Remediation = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-checklist'
        $instanceResult.TargetResourceID = 'NodeNameMgmtIPConfiguration'
        $instanceResult.TargetResourceName = 'NodeNameMgmtIPConfiguration'
        $instanceResult.TargetResourceType = 'NodeMgmtIPConfiguration'
        $instanceResult.Timestamp = [datetime]::UtcNow
        $instanceResult.HealthCheckSource = $ENV:EnvChkrId
        $instanceResult.Status = if ($AdditionalData.Status -contains 'Failed') { 'Failed' } else { 'Succeeded' }
        $instanceResult.AdditionalData = $AdditionalData
        return $instanceResult
    }
    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."

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

            $AdditionalData += New-Object -TypeName PSObject -Property @{
                Source    = $intent.Host
                Resource  = 'AddNodeIntentStatusCheck'
                Detail    = $TestNetworkIntentStatusDetail
                Status    = if ($intentHealthy) { 'Succeeded' } else { 'Failed' }
                TimeStamp = [datetime]::UtcNow
            }
        }

        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
        }

        $AdditionalData += New-Object -TypeName PSObject -Property @{
            Source    = $source
            Resource  = 'AddNodeStorageIntentCheck'
            Detail    = $TestNetworkIntentStatusDetail
            Status    = if ($null -eq $storageIntent) { 'Failed' } else { 'Succeeded' }
            TimeStamp = [datetime]::UtcNow
        }

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

        $AdditionalData += New-Object -TypeName PSObject -Property @{
            Source    = $PsSession.ComputerName
            Resource  = 'AddNodeNewNodeNetworkATCServiceCheck'
            Detail    = $TestNetworkATCServiceDetail
            Status    = if ($ATCStatusHealthy) { 'Succeeded' } else { 'Failed' }
            TimeStamp = [datetime]::UtcNow
        }

        $instanceResult = New-Object AzStackHciNetworkTarget
        $instanceResult.Name = 'AzStackHci_Network_Test_Network_AddNode_Intent_and_NetworkATC_Service'
        $instanceResult.Title = 'Test Network intent on existing node and if NetworkATC service is running on new node'
        $instanceResult.Severity = 'Critical'
        $instanceResult.Description = 'Fails if Network intent is unhealthy on existing nodes and NetworkATC Service is not running on new node'
        $instanceResult.Remediation = 'https://learn.microsoft.com/en-us/azure-stack/hci/deploy/deployment-tool-checklist'
        $instanceResult.TargetResourceID = 'NetworkIntentNetworkATCService'
        $instanceResult.TargetResourceName = 'NetworkIntentNetworkATCService'
        $instanceResult.TargetResourceType = 'NetworkIntentNetworkATCServiceType'
        $instanceResult.Timestamp = [datetime]::UtcNow
        $instanceResult.HealthCheckSource = $ENV:EnvChkrId
        $instanceResult.Status = if ($AdditionalData.Status -contains 'Failed') { 'Failed' } else { 'Succeeded' }
        $instanceResult.AdditionalData = $AdditionalData
        return $instanceResult
    }
    catch
    {
        throw $_
    }
}
# SIG # Begin signature block
# MIIoKgYJKoZIhvcNAQcCoIIoGzCCKBcCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCB82mjZWU7bQQL
# bBrR72SgivNopRMeSfjeyVz9Bi2HUKCCDXYwggX0MIID3KADAgECAhMzAAADTrU8
# esGEb+srAAAAAANOMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMwMzE2MTg0MzI5WhcNMjQwMzE0MTg0MzI5WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDdCKiNI6IBFWuvJUmf6WdOJqZmIwYs5G7AJD5UbcL6tsC+EBPDbr36pFGo1bsU
# p53nRyFYnncoMg8FK0d8jLlw0lgexDDr7gicf2zOBFWqfv/nSLwzJFNP5W03DF/1
# 1oZ12rSFqGlm+O46cRjTDFBpMRCZZGddZlRBjivby0eI1VgTD1TvAdfBYQe82fhm
# WQkYR/lWmAK+vW/1+bO7jHaxXTNCxLIBW07F8PBjUcwFxxyfbe2mHB4h1L4U0Ofa
# +HX/aREQ7SqYZz59sXM2ySOfvYyIjnqSO80NGBaz5DvzIG88J0+BNhOu2jl6Dfcq
# jYQs1H/PMSQIK6E7lXDXSpXzAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUnMc7Zn/ukKBsBiWkwdNfsN5pdwAw
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMDUxNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAD21v9pHoLdBSNlFAjmk
# mx4XxOZAPsVxxXbDyQv1+kGDe9XpgBnT1lXnx7JDpFMKBwAyIwdInmvhK9pGBa31
# TyeL3p7R2s0L8SABPPRJHAEk4NHpBXxHjm4TKjezAbSqqbgsy10Y7KApy+9UrKa2
# kGmsuASsk95PVm5vem7OmTs42vm0BJUU+JPQLg8Y/sdj3TtSfLYYZAaJwTAIgi7d
# hzn5hatLo7Dhz+4T+MrFd+6LUa2U3zr97QwzDthx+RP9/RZnur4inzSQsG5DCVIM
# pA1l2NWEA3KAca0tI2l6hQNYsaKL1kefdfHCrPxEry8onJjyGGv9YKoLv6AOO7Oh
# JEmbQlz/xksYG2N/JSOJ+QqYpGTEuYFYVWain7He6jgb41JbpOGKDdE/b+V2q/gX
# UgFe2gdwTpCDsvh8SMRoq1/BNXcr7iTAU38Vgr83iVtPYmFhZOVM0ULp/kKTVoir
# IpP2KCxT4OekOctt8grYnhJ16QMjmMv5o53hjNFXOxigkQWYzUO+6w50g0FAeFa8
# 5ugCCB6lXEk21FFB1FdIHpjSQf+LP/W2OV/HfhC3uTPgKbRtXo83TZYEudooyZ/A
# Vu08sibZ3MkGOJORLERNwKm2G7oqdOv4Qj8Z0JrGgMzj46NFKAxkLSpE5oHQYP1H
# tPx1lPfD7iNSbJsP6LiUHXH1MIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# 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
# /Xmfwb1tbWrJUnMTDXpQzTGCGgowghoGAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAANOtTx6wYRv6ysAAAAAA04wDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIAjMcD14T89tZSEfsGTpBXbw
# BOEWzNTC/84q31B25VT+MEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAMjatUty3e9nIYmXiur7QV+826IFtrmrASCBg9LcumNGjp/fmsk+2jyLZ
# D5y8Z8U/tAfqZ8xBW+9vtFQMxJ1WgDBQztHWXj5NlgVVWhaWgyEZ2+nFRBJeObLy
# J6olPiHTLFyLyxQJCezpUoWp0TvQtHvaDwv8Gg1bHTPJ2PR0Dh+cQvE9CUsRZu4W
# laoRU2Vvst1Eim16+IFizlBu0YA4S3TvVL0ZiyBrqrB3XL+FqdvwIU12e+FcVxFD
# B/7xP1VS4Cww4uFzkqeSZw92my76wfvEhKDV48GCDorPg7Tx9jKPD9Vx8sPPg9ph
# 0fnLWr0WnS03qigknrsny5ai4hcaeKGCF5QwgheQBgorBgEEAYI3AwMBMYIXgDCC
# F3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq
# hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCAGYCzji5AzWzPWHvNJsU07ocdIVkFvvRv5rRFlylnHGwIGZQQDzWyA
# GBMyMDIzMDkyMjA4MzExOS4yNjhaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l
# cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046REMwMC0w
# NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg
# ghHqMIIHIDCCBQigAwIBAgITMwAAAdIhJDFKWL8tEQABAAAB0jANBgkqhkiG9w0B
# AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMzA1MjUxOTEy
# MjFaFw0yNDAyMDExOTEyMjFaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z
# MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046REMwMC0wNUUwLUQ5NDcxJTAjBgNV
# BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQDcYIhC0QI/SPaT5+nYSBsSdhBPO2SXM40Vyyg8Fq1T
# PrMNDzxChxWUD7fbKwYGSsONgtjjVed5HSh5il75jNacb6TrZwuX+Q2++f2/8CCy
# u8TY0rxEInD3Tj52bWz5QRWVQejfdCA/n6ZzinhcZZ7+VelWgTfYC7rDrhX3TBX8
# 9elqXmISOVIWeXiRK8h9hH6SXgjhQGGQbf2bSM7uGkKzJ/pZ2LvlTzq+mOW9iP2j
# cYEA4bpPeurpglLVUSnGGQLmjQp7Sdy1wE52WjPKdLnBF6JbmSREM/Dj9Z7okxRN
# UjYSdgyvZ1LWSilhV/wegYXVQ6P9MKjRnE8CI5KMHmq7EsHhIBK0B99dFQydL1vd
# uC7eWEjzz55Z/DyH6Hl2SPOf5KZ4lHf6MUwtgaf+MeZxkW0ixh/vL1mX8VsJTHa8
# AH+0l/9dnWzFMFFJFG7g95nHJ6MmYPrfmoeKORoyEQRsSus2qCrpMjg/P3Z9WJAt
# FGoXYMD19NrzG4UFPpVbl3N1XvG4/uldo1+anBpDYhxQU7k1gfHn6QxdUU0TsrJ/
# JCvLffS89b4VXlIaxnVF6QZh+J7xLUNGtEmj6dwPzoCfL7zqDZJvmsvYNk1lcbyV
# xMIgDFPoA2fZPXHF7dxahM2ZG7AAt3vZEiMtC6E/ciLRcIwzlJrBiHEenIPvxW15
# qwIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFCC2n7cnR3ToP/kbEZ2XJFFmZ1kkMB8G
# A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG
# Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy
# MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w
# XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy
# dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG
# A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD
# AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQCw5iq0Ey0LlAdz2PcqchRwW5d+fitNISCv
# qD0E6W/AyiTk+TM3WhYTaxQ2pP6Or4qOV+Du7/L+k18gYr1phshxVMVnXNcdjecM
# tTWUOVAwbJoeWHaAgknNIMzXK3+zguG5TVcLEh/CVMy1J7KPE8Q0Cz56NgWzd9ur
# G+shSDKkKdhOYPXF970Mr1GCFFpe1oXjEy6aS+Heavp2wmy65mbu0AcUOPEn+hYq
# ijgLXSPqvuFmOOo5UnSV66Dv5FdkqK7q5DReox9RPEZcHUa+2BUKPjp+dQ3D4c9I
# H8727KjMD8OXZomD9A8Mr/fcDn5FI7lfZc8ghYc7spYKTO/0Z9YRRamhVWxxrIsB
# N5LrWh+18soXJ++EeSjzSYdgGWYPg16hL/7Aydx4Kz/WBTUmbGiiVUcE/I0aQU2U
# /0NzUiIFIW80SvxeDWn6I+hyVg/sdFSALP5JT7wAe8zTvsrI2hMpEVLdStFAMqan
# FYqtwZU5FoAsoPZ7h1ElWmKLZkXk8ePuALztNY1yseO0TwdueIGcIwItrlBYg1Xp
# Pz1+pMhGMVble6KHunaKo5K/ldOM0mQQT4Vjg6ZbzRIVRoDcArQ5//0875jOUvJt
# Yyc7Hl04jcmvjEIXC3HjkUYvgHEWL0QF/4f7vLAchaEZ839/3GYOdqH5VVnZrUIB
# QB6DTaUILDCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI
# hvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# MjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAy
# MDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
# AQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25Phdg
# M/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPF
# dvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6
# GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBp
# Dco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50Zu
# yjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3E
# XzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0
# lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1q
# GFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ
# +QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PA
# PBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkw
# EgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxG
# NSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARV
# MFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWlj
# cm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAK
# BggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC
# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX
# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v
# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI
# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG
# 9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0x
# M7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmC
# VgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449
# xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wM
# nosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDS
# PeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2d
# Y3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxn
# GSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+Crvs
# QWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokL
# jzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL
# 6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNN
# MIICNQIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEn
# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOkRDMDAtMDVFMC1EOTQ3MSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQCJ
# ptLCZsE06NtmHQzB5F1TroFSBqCBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA6Le8iDAiGA8yMDIzMDkyMjA3MTEw
# NFoYDzIwMjMwOTIzMDcxMTA0WjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDot7yI
# AgEAMAcCAQACAiIVMAcCAQACAhQhMAoCBQDouQ4IAgEAMDYGCisGAQQBhFkKBAIx
# KDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZI
# hvcNAQELBQADggEBAIX3iWYeNwmEkF0AgEnhcb1lXq5+ON1FrmRtRGAk6dA72cRG
# LRWERZdI+EWS+t2TAweOW5L4eWYukhTu5yDM6y1p69WgV9b0QmZI90CiqqqNVl9J
# cNbh8fjH24Gxe4u5WbVKRR6pVp0IIUEQ91zkHaaBCCySsjKVx8giknb0JYfP4Ou4
# XSKkObcUbdvfzqqlnVpnhUW0zKmcgqJrSdP3wwKqpNIKG47q6iGR4nGI8wMMho0H
# JGM9p4YMtLQO0KZkXNGccH/y94cOUhH+OUbjFZAZIHD975oN7mUfjLPrdIQTcvwA
# DAxYrXC3cEZbEQmfJoY5J28KJcVv7hDqzzAKJ2cxggQNMIIECQIBATCBkzB8MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy
# b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAdIhJDFKWL8tEQABAAAB0jAN
# BglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8G
# CSqGSIb3DQEJBDEiBCBTkR2uW/9jjBRyHVAfRuvCL5SrqVD/WeLW1pSzqI8rvzCB
# +gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIMeAIJPf30i9ZbOExU557GwWNaLH
# 0Z5s65JFga2DeaROMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh
# c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD
# b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw
# MTACEzMAAAHSISQxSli/LREAAQAAAdIwIgQgo/VOETe+zJRyyikx/HdHN03ONGrb
# FbBfi0eUT2omsFUwDQYJKoZIhvcNAQELBQAEggIAMN82x3BCVxePcyxRBOZJUD6i
# VakrJk/zI0qaeaIVl7dsYLYbWHuhsUwsPrvl0rnJr2H7ZS1oTXg7RvQ7O7f3ayKP
# WkZ2nEWiAz2fjX3aMzl+rsHIwm03UXOdlDzh50sKDUV0uYcPv7ca4tyGEilbX/Va
# jEnXi65xTWAvLI0diELe6o1uky4JvaidoJQGlYTM7OFEmttwvx3+0ek11S10/+On
# IbsWAuY8oa52rgfEdkD+aAV5A/WHOXW/XLEIEY6Lv6ckgt2tJNARhg+KhlPd/AIv
# EKvpueFEEI69SCHaUHCQCdkqBo4gr0oGG+N8SGjYrtPjxw0reKOhm/iBO9BpDKDy
# mZHLN2EDOQDtiu510UmOXQt4NW9jjzw5iT6xxof9ZvnMgmEa5xSn837/On7XU5V5
# ejQ0nvPHNam5Dq+RbTb4qvAayWz4vbEWTDdSLuZ9Uhhvwj1ALL7FwnGJ0R5Ytf66
# elFmhwhHL4pDBgu2W8tPH1H9VJPC5P8vbvxEJGsX0k75TeBk0h3brrnJHQHDWxvb
# +ipwG6M1Cn/b16zK+GX81wqbm/RW1EngrZhpmD7Jmp86nInSPb3/VyFgottftzAv
# 2QjiOGH10hm8QPIutHb+Sm22CtNRu7ZzcZpTwuIPdZCODNUL0z6l6qxVNZaXoV6D
# 7mF6Pbz5B2Z9q8nytCQ=
# SIG # End signature block