NetworkValidation/Microsoft.AzureStack.ReadinessChecker.NetworkValidation.psm1

<#############################################################
# #
# Copyright (C) Microsoft Corporation. All rights reserved. #
# #
#############################################################>


<#
.SYNOPSIS
    Verifies network infrastructure readiness for Azure Stack deployment.
.DESCRIPTION
    Performs series of network tests from a device connected to the border switches to verify that network configuration prerequisites are met.
.EXAMPLE
    Invoke-AzsNetworkValidation -SkipTests 'DnsServer' -TimeServer pool.ntp.org
    This example runs Azure Stack Edge network validation, skips the DNS Server test, and saves the report in the default location
.EXAMPLE
    Invoke-AzsNetworkValidation -DeploymentDataPath D:\AzureStack\DeploymentData.json -CleanReport -OutputPath C:\Temp\
    This example runs Azure Stack Hub network validation, creates a new report, and saves it to C:\Temp folder
.NOTES
    This feature is in preview and may not produce the expected results.
#>


$global:ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
$global:ProgressPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue

function Invoke-AzsNetworkValidation {
    [CmdletBinding(DefaultParameterSetName = 'Edge')]
    param (
        # Path to Azure Stack Hub deployment configuration file created by the Deployment Worksheet.
        [Parameter(Mandatory = $true, ParameterSetName = 'Hub')]
        [System.String]
        $DeploymentDataPath,

        # List of test to run. Default is to run all tests.
        [Parameter()]
        [ValidateSet('LinkLayer', 'IPConfig', 'DnsServer', 'TimeServer', 'DuplicateIP', 'Proxy', 'AzureEndpoint', 'WindowsUpdateServer', 'DnsRegistration')]
        [System.String[]]
        $RunTests,

        # List of test to skip. Default is to run all tests.
        [Parameter()]
        [ValidateSet('LinkLayer', 'IPConfig', 'DnsServer', 'TimeServer', 'DuplicateIP', 'AzureEndpoint', 'WindowsUpdateServer', 'DnsRegistration')]
        [System.String[]]
        $SkipTests,

        # DNS Server address(es)
        [Parameter(ParameterSetName = 'Edge')]
        [System.String[]]
        $DnsServer,

        # DNS name to resolve for DNS test
        [Parameter()]
        [System.String]
        $DnsName = 'management.azure.com',

        # Fully qualified domain name of the Edge device
        [Parameter(ParameterSetName = 'Edge')]
        [System.String]
        $DeviceFqdn,

        # Time Server address(es)
        [Parameter(ParameterSetName = 'Edge')]
        [System.String[]]
        $TimeServer,

        # Compute IP range(s) to be used by Kubernetes specified as Start IP and End IP separated by a hyphen
        # Example: '10.128.44.241-10.128.44.245'
        [Parameter(ParameterSetName = 'Edge')]
        [System.String[]]
        $ComputeIPs,

        # Azure cloud - specify to use a sovereign or custom Azure cloud
        [Parameter()]
        [ValidateSet('AzureCloud', 'AzureChinaCloud', 'AzureGermanCloud', 'AzureUSGovernment', 'CustomCloud')]
        [System.String]
        $AzureEnvironment = 'AzureCloud',

        # Azure Resource Manager endpoint URI for custom cloud
        [Parameter()]
        [System.Uri]
        $CustomCloudArmEndpoint,

        # URI of an HTTP proxy server
        [Parameter(ParameterSetName = 'Edge')]
        [System.Uri]
        $Proxy,

        # Azure Resource Manager endpoint URI for custom cloud
        [Parameter(ParameterSetName = 'Edge')]
        [System.Management.Automation.PSCredential]
        $ProxyCredential,

        # URI to test the proxy server
        [Parameter(ParameterSetName = 'Edge')]
        [System.Uri]
        $ExternalUri = 'https://management.azure.com/metadata/endpoints?api-version=2015-01-01',

        # Windows Update Services server URI
        [Parameter(ParameterSetName = 'Edge')]
        [System.Uri[]]
        $WindowsUpdateServer = @(
            'http://windowsupdate.microsoft.com'
            'http://update.microsoft.com'
            'https://update.microsoft.com'
            'http://download.windowsupdate.com'
            'https://download.microsoft.com'
            'http://go.microsoft.com'
            'http://dl.delivery.mp.microsoft.com'
        ),

        # Directory path for log and report output
        [Parameter(HelpMessage = 'Directory path for log and report output')]
        [System.String]
        $OutputPath = "$env:TEMP\AzsReadinessChecker",

        # Remove all previous progress and create a clean report
        [Parameter(HelpMessage = 'Remove all previous progress and create a clean report')]
        [switch]
        $CleanReport = $false
    )

    try {
        #region Initialization
        $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop
        $ProgressPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue
        $Global:OutputPath = $OutputPath
        Import-Module -Name $PSScriptRoot\..\Microsoft.AzureStack.ReadinessChecker.Reporting.psm1 -Force
        Write-Header -Invocation $MyInvocation -Params $PSBoundParameters
        $readinessReport = Get-AzsReadinessProgress -Clean:$CleanReport
        $readinessReport = Add-AzsReadinessCheckerJob -Report $readinessReport
        $defaultHubTests = @('LinkLayer', 'IPConfig', 'DnsServer', 'TimeServer', 'DuplicateIP', 'AzureEndpoint')
        $defaultEdgeTests = @('LinkLayer', 'IPConfig', 'DnsServer', 'TimeServer', 'DuplicateIP', 'AzureEndpoint', 'WindowsUpdateServer', 'DnsRegistration')
        $solutionType = $PSCmdlet.ParameterSetName
        $webParams = @{'TimeoutSec' = 15; 'UseBasicParsing' = $true}

        if ($Proxy) {
            $webParams += @{'Proxy' = $Proxy}

            if ($ProxyCredential) {
                $webParams += @{'ProxyCredential' = $ProxyCredential}
            }
        }

        Write-AzsReadinessLog -Message "Starting Azure Stack $solutionType network validation"

        # Determine which tests to run
        if (-not $RunTests) {
            Write-AzsReadinessLog -Message "Parameter RunTests not specified. Executing default set of tests for solution '$solutionType'."

            if ($solutionType -eq 'Edge') {
                $RunTests = $defaultEdgeTests
            }
            elseif ($solutionType -eq 'Hub') {
                $RunTests = $defaultHubTests
            }
        }

        foreach ($test in $RunTests) {
            if (($solutionType -eq 'Edge' -and $test -notin $defaultEdgeTests) -or ($solutionType -eq 'Hub' -and $test -notin $defaultHubTests)) {
                Write-AzsReadinessLog -Message "Test $test is not applicable to solution 'Azure Stack $solutionType'. It will be skipped." -Type 'Warning' -ToScreen
                $RunTests = $RunTests -ne $test
            }

            if ($test -in $SkipTests) {
                Write-AzsReadinessLog -Message "Will skip test '$test'"
                $RunTests = $RunTests -ne $test
            }
        }

        if ($Proxy) {
            $RunTests += 'Proxy'
        }

        Write-AzsReadinessLog -Message "The following tests will be executed: $($RunTests -join ', ')" -ToScreen
        #endregion

        #region ParameterValidation
        # Read values from deployment data (if provided) and validate input parameters
        Write-AzsReadinessLog -Message "Validating input parameters" -ToScreen
        $paramValidation = $true

        if ($DeploymentDataPath) {
            Write-AzsReadinessLog -Message "Validating parameter 'DeploymentDataPath' value '$DeploymentDataPath'"

            if (Test-Path -Path $DeploymentDataPath -PathType Leaf) {
                Write-AzsReadinessLog -Message "Reading deployment configuration file '$DeploymentDataPath'"

                try {
                    $deploymentData = Get-Content -Path $DeploymentDataPath -Raw | ConvertFrom-Json
                    $DnsServer = $deploymentData.ScaleUnits.DeploymentData.DnsForwarder
                    $TimeServer = $deploymentData.ScaleUnits.DeploymentData.TimeServer
                    $torAddresses = (($deploymentData.ConfigData.InputData.Supernets.Networks |
                        Where-Object {$_.Name -like "CL01-P2P_Rack00/Edge?_To_Rack01/Tor?"}).Assignments |
                        Where-Object {$_.Name -like "*/Tor?"}).IPv4Address
                    $addressRanges = @()

                    foreach ($supernet in ($deploymentData.ConfigData.InputData.Supernets | Where-Object {$_.Name -notlike "*-Private"}).IPv4) {
                        $addressRanges += "$($supernet.Network)-$($supernet.Broadcast)"
                    }

                    if ($deploymentData.ScaleUnits.DeploymentData.UseAdfs -eq 'Selected') {
                        $RunTests = $RunTests -ne 'AzureEndpoint'
                    }
                }
                catch {
                    Write-AzsReadinessLog "Unable to read JSON from '$DeploymentDataPath'" -Type 'Error' -ToScreen
                    $paramValidation = $false
                }
            }
            else {
                Write-AzsReadinessLog -Message "Deployment Data file not found: $DeploymentDataPath" -Type 'Error' -ToScreen
                $paramValidation = $false
            }
        }

        if ($RunTests -contains 'DnsServer') {
            if ($DnsServer) {
                Write-AzsReadinessLog -Message "Validating parameter 'DnsServer' value '$DnsServer'"

                foreach ($serverAddress in $DnsServer) {
                    Write-AzsReadinessLog -Message "Parsing DNS server value '$serverAddress'"
                    $parseAddress = $null
                    if ([System.Net.IPAddress]::TryParse($serverAddress, [ref]$parseAddress)) {
                        Write-AzsReadinessLog -Message "'$serverAddress' is a valid IP address"
                    }
                    else {
                        Write-AzsReadinessLog -Message "'$serverAddress' is not a valid IP address" -Type 'Error' -ToScreen
                        $paramValidation = $false
                    }
                }
            }
            else {
                Write-AzsReadinessLog -Message "Parameter missing: DnsServer" -Type 'Error' -ToScreen
                $paramValidation = $false
            }

            Write-AzsReadinessLog -Message "Validating parameter 'DnsName' value '$DnsName'"

            if ([System.Uri]::CheckHostName($DnsName) -eq [System.UriHostNameType]::Dns) {
                Write-AzsReadinessLog -Message "'$DnsName' is a valid DNS name"
            }
            else {
                Write-AzsReadinessLog -Message "'$DnsName' is not a valid DNS name" -Type 'Error' -ToScreen
                $paramValidation = $false
            }
        }

        if ($RunTests -contains 'TimeServer') {
            if ($TimeServer) {
                Write-AzsReadinessLog -Message "Validating parameter 'TimeServer' value '$TimeServer'"

                foreach ($ntpAddress in $TimeServer) {
                    Write-AzsReadinessLog -Message "Parsing time server value '$ntpAddress'"
                    if ([System.Uri]::CheckHostName($ntpAddress) -in @([System.UriHostNameType]::Dns, [System.UriHostNameType]::IPv4)) {
                        Write-AzsReadinessLog -Message "'$ntpAddress' is a valid address of type '$([System.Uri]::CheckHostName($ntpAddress))'"
                    }
                    else {
                        Write-AzsReadinessLog -Message "'$ntpAddress' is not a valid IP address or DNS name" -Type 'Error' -ToScreen
                        $paramValidation = $false
                    }
                }
            }
            else {
                Write-AzsReadinessLog -Message 'Parameter missing: TimeServer' -Type 'Error' -ToScreen
                $paramValidation = $false
            }
        }

        if ($RunTests -contains 'DuplicateIP') {
            if ($solutionType -eq 'Edge') {
                if ($ComputeIPs) {
                    Write-AzsReadinessLog -Message "Validating parameter 'ComputeIPs' value '$ComputeIPs'"
                    $addressRanges = $ComputeIPs
                }
                else {
                    Write-AzsReadinessLog -Message "Parameter missing: ComputeIPs. Use -SkipTest DuplicateIPs to skip the test" -Type 'Error' -ToScreen
                    $paramValidation = $false
                }
            }

            foreach ($addressRange in $addressRanges) {
                $rangeSplit = $addressRange.Split('-')

                if ($rangeSplit.Count -ne 2) {
                    Write-AzsReadinessLog -Message "Incorrect parameter value: '$addressRange'. Provide the value in the following format: 'X.X.X.X-Y.Y.Y.Y" -Type 'Error' -ToScreen
                    $paramValidation = $false
                }

                $bgnIp = $rangeSplit[0].Trim()
                $endIp = $rangeSplit[1].Trim()
                $parseAddress = $null

                foreach ($ipAddress in @($bgnIp, $endIp)) {
                    if (-not [System.Net.IPAddress]::TryParse($ipAddress, [ref]$parseAddress)) {
                        Write-AzsReadinessLog -Message "'$ipAddress' is not a valid IP address" -Type 'Error' -ToScreen
                        $paramValidation = $false
                    }
                }

                if ($paramValidation) {
                    # This double conversion is done to reverse the byte order of the integer Address property
                    $bgnIpInt = ([System.Net.IPAddress][System.String]([System.Net.IPAddress]$bgnIp).Address).Address
                    $endIpInt = ([System.Net.IPAddress][System.String]([System.Net.IPAddress]$endIp).Address).Address

                    if ($endIpInt -lt $bgnIpInt) {
                        Write-AzsReadinessLog -Message "Incorrect parameter value: '$addressRange'. The beginning of range cannot be lower than the end of range." -Type 'Error' -ToScreen
                        $paramValidation = $false
                    }
                }
            }
        }

        if ($RunTests -contains 'Proxy') {
            Write-AzsReadinessLog -Message "Validating parameter 'Proxy' value '$Proxy'"
            $paramValidation = $paramValidation -and (Test-Uri -Uri $Proxy -SupportedProtocols 'http')
            $paramValidation = $paramValidation -and (Test-Uri -Uri $ExternalUri)
        }

        if ($RunTests -contains 'AzureEndpoint' -and $AzureEnvironment -eq 'CustomCloud') {
            Write-AzsReadinessLog -Message "Validating parameter 'CustomCloudArmEndpoint' value '$CustomCloudArmEndpoint'"

            if (-not $CustomCloudArmEndpoint) {
                Write-AzsReadinessLog -Message 'Parameter missing: CustomCloudArmEndpoint' -Type 'Error' -ToScreen
                $paramValidation = $false
            }

            $paramValidation = $paramValidation -and (Test-Uri -Uri $CustomCloudArmEndpoint)
        }

        if ($RunTests -contains 'WindowsUpdateServer') {
            Write-AzsReadinessLog -Message "Validating parameter 'WindowsUpdateServer' value '$($WindowsUpdateServer -join ', ')'"

            foreach ($uri in $WindowsUpdateServer) {
                Write-AzsReadinessLog -Message "Parsing Windows Update Server value '$uri'"
                $paramValidation = $paramValidation -and (Test-Uri -Uri $uri)
            }
        }

        if ($RunTests -contains 'DnsRegistration') {
            if ($DeviceFqdn) {
                Write-AzsReadinessLog -Message "Validating parameter 'DeviceFqdn' value '$DeviceFqdn'"

                if ([System.Uri]::CheckHostName($DeviceFqdn) -eq [System.UriHostNameType]::Dns) {
                    Write-AzsReadinessLog -Message "'$DeviceFqdn' is a valid DNS name"
                }
                else {
                    Write-AzsReadinessLog -Message "'$DeviceFqdn' is not a valid DNS name" -Type 'Error' -ToScreen
                    $paramValidation = $false
                }
            }
            else {
                Write-AzsReadinessLog -Message "Parameter missing: DeviceFqdn" -Type 'Error' -ToScreen
                $paramValidation = $false
            }
        }

        if (-not $paramValidation) {
            throw 'Parameter validation failed'
        }
        #endregion

        #region Main
        Write-AzsReadinessLog -Message "Validating Azure Stack $solutionType Network Readiness" -ToScreen
        $result = @()

        if ($RunTests -contains 'LinkLayer') {
            $testResult = Test-LinkLayer
            Write-AzsResult -In $testResult
            $result += $testResult

            # If the link layer test fails, no other tests are applicable
            if ($testResult.Result -eq 'Fail') {
                throw 'No network connection'
            }
        }

        if ($RunTests -contains 'IPConfig') {
            $testResult = Test-IPConfig
            Write-AzsResult -In $testResult
            $result += $testResult

            # If the ip config test fails, no other tests are applicable
            if ($testResult.Result -eq 'Fail') {
                throw 'Invalid IP configuration'
            }
        }

        if ($RunTests -contains 'DuplicateIP') {
            $testResult = Test-DuplicateIP -AddressRanges $addressRanges -SkipAddress $torAddresses
            Write-AzsResult -In $testResult
            $result += $testResult
        }

        # If more than one interface is connected, all connectivity tests are performed from each interface, with all other interfaces shut down
        $ipRoutes = Get-NetRoute -AddressFamily IPv4 | Where-Object {$_.NextHop -ne '0.0.0.0'}
        $routedInterfaces = Get-NetIPInterface -AddressFamily IPv4 | Where-Object {$_.ConnectionState -eq 'Connected' -and $_.InterfaceIndex -in $ipRoutes.InterfaceIndex}
        $netAdapters = Get-NetAdapter | Where-Object {$_.InterfaceIndex -in $routedInterfaces.InterfaceIndex} | Select-Object -Unique | Sort-Object -Property 'Name'
        $disabledNetAdapters = @()

        foreach ($netAdapter in $netAdapters) {
            Write-AzsReadinessLog -Message " Using network adapter name '$($netAdapter.Name)', description '$($netAdapter.InterfaceDescription)'" -ToScreen

            if ((Get-NetAdapter -InterfaceIndex $netAdapter.InterfaceIndex).Status -ne 'Up') {
                Write-AzsReadinessLog -Message "Enabling network adapter name '$($netAdapter.Name)', description '$($netAdapter.InterfaceDescription)'"
                Enable-NetAdapter -InputObject $netAdapter
                $disabledNetAdapters = $disabledNetAdapters -ne $netAdapter.Name
            }

            foreach ($netAdapterToDisable in $netAdapters | Where-Object {$_.InterfaceIndex -ne $netAdapter.InterfaceIndex}) {
                if ((Get-NetAdapter -InterfaceIndex $netAdapterToDisable.InterfaceIndex).Status -eq 'Up') {
                    Write-AzsReadinessLog -Message "Disabling network adapter name '$($netAdapterToDisable.Name)', description '$($netAdapterToDisable.InterfaceDescription)'"
                    Disable-NetAdapter -InputObject $netAdapterToDisable -Confirm:$false
                    $disabledNetAdapters += $netAdapterToDisable.Name
                }
            }

            if ($RunTests -contains 'DnsServer') {
                foreach ($serverAddress in $DnsServer) {
                    $testResult = Test-DnsServer -DnsServer $serverAddress -ResolveName $DnsName
                    Write-AzsResult -In $testResult
                    $result += $testResult
                }
            }

            if ($RunTests -contains 'TimeServer') {
                foreach ($serverAddress in $TimeServer) {
                    $testResult = Test-TimeServer -TimeServer $serverAddress
                    Write-AzsResult -In $testResult
                    $result += $testResult
                }
            }

            if ($RunTests -contains 'Proxy') {
                $testResult = Test-ProxyServer -Proxy $Proxy -ProxyCredential $ProxyCredential -TestUri $ExternalUri
                Write-AzsResult -In $testResult
                $result += $testResult
            }

            if ($RunTests -contains 'AzureEndpoint') {
                foreach ($endpointType in @('ARM', 'Graph', 'Login', 'ManagementService')) {
                    $testResult = Test-AzureEndpoint -AzureEnvironment $AzureEnvironment -CustomCloudArmEndpoint $CustomCloudArmEndpoint -EndpointType $endpointType
                    Write-AzsResult -In $testResult
                    $result += $testResult
                }
            }

            if ($RunTests -contains 'WindowsUpdateServer') {
                foreach ($serverAddress in $WindowsUpdateServer) {
                    $testResult = Test-WindowsUpdateServer -ServerUri $serverAddress
                    Write-AzsResult -In $testResult
                    $result += $testResult
                }
            }
        }

        if ($RunTests -contains 'DnsRegistration') {
            foreach ($dnsRecord in @('', 'login.', 'management.', '*.blob.', 'compute.')) {
                $testResult = Test-DnsRegistration -DnsServer $DnsServer -DnsRecord ($dnsRecord + $DeviceFqdn)
                Write-AzsResult -In $testResult
                $result += $testResult
            }
        }
        #endregion
    }
    catch {
        Write-AzsReadinessLog -Message "Test failed with error: $($_.Exception.Message)" -Type 'Error' -ToScreen
    }
    finally {
        if ($disabledNetAdapters) {
            Write-AzsReadinessLog -Message "Re-enabling previously disabled network adapters" -ToScreen
            Enable-NetAdapter -Name $disabledNetAdapters
        }

        # Write results to readiness report
        $readinessReport.NetworkValidation = $result
        $readinessReport = Close-AzsReadinessCheckerJob -Report $readinessReport
        Write-AzsReadinessProgress -Report $readinessReport
        Write-AzsReadinessReport -Report $readinessReport
        Write-Footer -invocation $MyInvocation
    }
}

<#
.SYNOPSIS
    Verifies link layer connectivity.
.DESCRIPTION
    Lists all network adapters and their link state and speed.
    Checks that at least one Ethernet network adapter is connected to a network.
#>

function Test-LinkLayer {
    [CmdletBinding()]
    param ()

    $test = 'Link Layer'
    Write-AzsReadinessLog -Message "Starting test '$test'"
    $netAdapterProperties = @(
        'Name'
        'InterfaceDescription'
        'InterfaceIndex'
        'MacAddress'
        'MediaType'
        'PhysicalMediaType'
        'Status'
        'AdminStatus'
        'LinkSpeed'
        'MediaConnectionState'
        'ConnectorPresent'
        'DriverInformation'
        'DriverFileName'
    )

    try {
        $netAdapters = Get-NetAdapter | Select-Object -Property $netAdapterProperties
        $outputObject = @{'NetAdapters' = $netAdapters}

        foreach ($netAdapter in $netAdapters) {
            Write-AzsReadinessLog -Message "Found network interface: Name='$($netAdapter.Name)', Description='$($netAdapter.InterfaceDescription)', Status='$($netAdapter.Status)', LinkSpeed=$($netAdapter.LinkSpeed)'"
        }

        if ($connectedEthernet = @($netAdapters | Where-Object {$_.PhysicalMediaType -eq '802.3' -and $_.Status -eq 'Up'})) {
            $result = 'OK'
            Write-AzsReadinessLog -Message "Found $($connectedEthernet.Count) connected physical network interfaces"
        }
        else {
            $result = 'Fail'
            $failureDetail = 'No connected ethernet adapters found'
            Write-AzsReadinessLog -Message "Test failed with error: $failureDetail" -Type 'Error'
        }
    }
    catch {
        $result = 'Fail'
        $failureDetail = $_.Exception.Message
        Write-AzsReadinessLog -Message "Test failed with error: $failureDetail" -Type 'Error'
    }

    Write-AzsReadinessLog -Message "Test '$test' finished, result '$result'"
    $hash = @{'Test' = $test; 'Result' = $result; 'FailureDetail' = $failureDetail; 'outputObject' = $outputObject}
    $object = New-Object -TypeName 'PSObject' -Property $hash
    return $object
}

<#
.SYNOPSIS
    Verifies IP configuration.
.DESCRIPTION
    Lists all network adapters that have an IP address assigned.
    Checks that at least one Ethernet network adapter is configured with a default gateway.
#>

function Test-IPConfig {
    [CmdletBinding()]
    param ()

    $test = 'IP Configuration'
    Write-AzsReadinessLog -Message "Starting test '$test'"
    $ipInterfaceProperties = @(
        'InterfaceIndex'
        'InterfaceAlias'
        'CompartmentId'
        'AddressFamily'
        'Forwarding'
        'ClampMss'
        'Advertising'
        'NlMtu'
        'AutomaticMetric'
        'InterfaceMetric'
        'NeighborDiscoverySupported'
        'NeighborUnreachabilityDetection'
        'BaseReachableTime'
        'ReachableTime'
        'RetransmitTime'
        'DadTransmits'
        'DadRetransmitTime'
        'RouterDiscovery'
        'ManagedAddressConfiguration'
        'OtherStatefulConfiguration'
        'WeakHostSend'
        'WeakHostReceive'
        'IgnoreDefaultRoutes'
        #'AdvertisedRouterLifetime'
        'AdvertiseDefaultRoute'
        'CurrentHopLimit'
        'ForceArpNdWolPattern'
        'DirectedMacWolPattern'
        'EcnMarking'
        'Dhcp'
        'ConnectionState'
    )
    $ipAddressProperties = @(
        'IPAddress'
        'InterfaceIndex'
        'InterfaceAlias'
        'AddressFamily'
        'Type'
        'PrefixLength'
        'PrefixOrigin'
        'SuffixOrigin'
        'AddressState'
        #'ValidLifetime'
        #'PreferredLifetime'
        'SkipAsSource'
    )
    $netRouteProperties = @(
        'DestinationPrefix'
        'InterfaceIndex'
        'InterfaceAlias'
        'CompartmentId'
        'AddressFamily'
        'NextHop'
        'Publish'
        'RouteMetric'
        'Protocol'
        #'ValidLifetime'
        #'PreferredLifetime'
    )

    try {
        $ipInterfaces = Get-NetIPInterface -AddressFamily IPv4 | Where-Object {$_.InterfaceIndex -gt 1} | Select-Object -Property $ipInterfaceProperties
        $ipAddresses = Get-NetIPAddress -AddressFamily IPv4 | Where-Object {$_.InterfaceIndex -gt 1} | Select-Object -Property $ipAddressProperties
        $ipRoutes = Get-NetRoute -AddressFamily IPv4 | Where-Object {$_.NextHop -ne '0.0.0.0'} | Select-Object -Property $netRouteProperties
        $outputObject = @{'IPInterfaces' = $ipInterfaces; 'IPAddresses' = $ipAddresses; 'IPRoutes' = $ipRoutes}

        foreach ($ipInterface in $ipInterfaces) {
            $ipAddress = $ipAddresses | Where-Object {$_.InterfaceIndex -eq $ipInterface.InterfaceIndex}
            $ipRoute = $ipRoutes | Where-Object {$_.InterfaceIndex -eq $ipInterface.InterfaceIndex -and $_.NextHop -ne '0.0.0.0'} | Select-Object -Property @{n = 'Route'; e = {$_.DestinationPrefix + ' > ' + $_.NextHop}}
            Write-AzsReadinessLog -Message "Found IP interface: Name='$($ipInterface.InterfaceAlias)', IPAddresses='$($ipAddress.IPAddress -join ', ')', DHCP='$($ipInterface.Dhcp)', ConnectionState='$($ipInterface.ConnectionState)', Routes='$($ipRoute.Route -join ', ')'"
        }

        if ($routedInterfaces = @($ipInterfaces | Where-Object {$_.ConnectionState -eq 'Connected' -and $_.InterfaceIndex -in $ipRoutes.InterfaceIndex})) {
            $result = 'OK'
            Write-AzsReadinessLog -Message "Found $($routedInterfaces.Count) connected IP-enabled interfaces with routing configuration"
        }
        else {
            $result = 'Fail'
            $failureDetail = 'No IP-enabled network interfaces with routing configuation are found'
            Write-AzsReadinessLog -Message "Test failed with error: $failureDetail" -Type 'Error'
        }
    }
    catch {
        $result = 'Fail'
        $failureDetail = $_.Exception.Message
        Write-AzsReadinessLog -Message "Test failed with error: $failureDetail" -Type 'Error'
    }

    Write-AzsReadinessLog -Message "Test '$test' finished, result '$result'"
    $hash = @{'Test' = $test; 'Result' = $result; 'FailureDetail' = $failureDetail; 'outputObject' = $outputObject}
    $object = New-Object -TypeName 'PSObject' -Property $hash
    return $object
}

<#
.SYNOPSIS
    Checks for duplicate IP address assignments.
.DESCRIPTION
    Pings IP addresses in the specified range to detect if they are already being used on the network.
#>

function Test-DuplicateIP {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.String[]]
        $AddressRanges,

        [Parameter()]
        [System.String[]]
        $SkipAddress
    )

    $test = 'Duplicate IP'
    Write-AzsReadinessLog -Message "Starting test '$test'"
    $netNeighborProperties = @(
        'InterfaceAlias'
        'InterfaceIndex'
        'IPAddress'
        'LinkLayerAddress'
        'State'
    )

    try {
        $addressList = @()
        $pingOutput = @()
        $result = 'OK'

        foreach ($addressRange in $AddressRanges) {
            $rangeSplit = $addressRange.Split('-')
            $bgnIp = $rangeSplit[0].Trim()
            $endIp = $rangeSplit[1].Trim()
            $bgnIpInt = ([System.Net.IPAddress][System.String]([System.Net.IPAddress]$bgnIp).Address).Address
            $endIpInt = ([System.Net.IPAddress][System.String]([System.Net.IPAddress]$endIp).Address).Address

            for ($ipInt = $bgnIpInt; $ipInt -le $endIpInt; $ipInt++) {
                $ipAddress = ([System.Net.IPAddress][System.String]$ipInt).IPAddressToString

                if ($ipAddress -notin $SkipAddresses) {
                    $addressList += $ipAddress
                }
            }
        }

        if ($addressList.Count -ge 100) {
            Write-AzsReadinessLog -Message "Will ping $($addressList.Count) IP addresses to detect potential conflict. The test will take a few minutes." -ToScreen
        }

        $ping = New-Object -TypeName System.Net.NetworkInformation.Ping
        $timeoutMs = 100
        $count = 0

        foreach ($ipAddress in $addressList) {
            $count++
            $pingResult = $ping.Send($ipAddress, $timeoutMs)
            $pingOutput += $pingResult | Select-Object -Property @{n = 'Address'; e = {$ipAddress}}, @{n = 'Status'; e = {$_.Status.ToString()}}, RoundTripTime

            if ($pingResult.Status -eq [System.Net.NetworkInformation.IPStatus]::Success) {
                Write-AzsReadinessLog -Message "IP Address '$ipAddress' responds to ping" -Type 'Warning'
                $result = 'Fail'
            }

            if ($count % 100 -eq 0) {
                Write-AzsReadinessLog -Message "Probed $count out of $($addressList.Count) IP addresses" -ToScreen
            }
        }

        $neighbors = Get-NetNeighbor | Where-Object {$_.State -eq 'Reachable'} | Select-Object -Property $netNeighborProperties
        $outputObject = @{'PingResults' = $pingOutput; 'NetNeighbors' = $neighbors}

        foreach ($ipAddress in $neighbors.IPAddress) {
            if ($ipAddress -in $addressList) {
                Write-AzsReadinessLog -Message "IP Address '$ipAddress' is active on the local network" -Type 'Warning'
                $result = 'Fail'
            }
        }

        if ($result -eq 'Fail') {
            $failureDetail = 'Some IP addresses allocated to Azure Stack are active on the network'
            Write-AzsReadinessLog -Message "Test failed with error: $failureDetail" -Type 'Error'
        }
    }
    catch {
        $result = 'Fail'
        $failureDetail = $_.Exception.Message
        Write-AzsReadinessLog -Message "Test failed with error: $failureDetail" -Type 'Error'
    }

    Write-AzsReadinessLog -Message "Test '$test' finished, result '$result'"
    $hash = @{'Test' = $test; 'Result' = $result; 'FailureDetail' = $failureDetail; 'outputObject' = $outputObject}
    $object = New-Object -TypeName 'PSObject' -Property $hash
    return $object
}

<#
.SYNOPSIS
    Verifies DNS server connectivity and name resolution.
.DESCRIPTION
    Checks that each DNS server can be reached over ICMP.
    Checks that each DNS server can be reached over TCP port 53.
    Tries to resolve a DNS name.
#>

function Test-DnsServer {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.String]
        $DnsServer,

        [Parameter(Mandatory = $true)]
        [System.String]
        $ResolveName
    )

    $test = "DNS Server $DnsServer"
    Write-AzsReadinessLog -Message "Starting test '$test'"
    $result = 'OK'

    try {
        Write-AzsReadinessLog -Message "Testing DNS server '$DnsServer'"
        $tnc = Test-NetConnectionEx -RemoteAddress $DnsServer -Port 53
        $dnsTest = Resolve-DnsName -Server $DnsServer -Name $ResolveName -DnsOnly
        Write-AzsReadinessLog -Message "Name '$ResolveName' resolved to IP addresses $($dnsTest.IPAddress -join ', ') using server $DnsServer"
    }
    catch {
        $result = 'Fail'
        $failureDetail = $_.Exception.Message
        Write-AzsReadinessLog -Message "Error resolving DNS name: '$failureDetail'" -Type 'Error'
    }
    finally {
        $outputObject = @{'NetConnection' = $tnc; 'DNSTest' = $dnsTest}
    }

    Write-AzsReadinessLog -Message "Test '$test' finished, result '$result'"
    $hash = @{'Test' = $test; 'Result' = $result; 'FailureDetail' = $failureDetail; 'outputObject' = $outputObject}
    $object = New-Object -TypeName 'PSObject' -Property $hash
    return $object
}

<#
.SYNOPSIS
    Verifies time server connectivity and probes for current time.
.DESCRIPTION
    Checks that each time server can be reached over ICMP.
    Tries to obtain time from the time server.
#>

function Test-TimeServer {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.String]
        $TimeServer
    )

    $test = "Time Server $TimeServer"
    Write-AzsReadinessLog -Message "Starting test '$test'"
    $result = 'OK'

    Write-AzsReadinessLog -Message "Testing time server '$TimeServer'"

    try {
        $tnc = Test-NetConnectionEx -RemoteAddress $TimeServer
        Write-AzsReadinessLog -Message "Trying to get NTP time from '$TimeServer'"
        $stripchart = & w32tm.exe /stripchart /computer:$TimeServer /dataonly /samples:1

        if ($stripchart.Count -ge 4 -and (($stripchart[3] -like '*, +*') -or ($stripchart[3] -like '*, -*'))) {
            Write-AzsReadinessLog -Message "Response received, current time and local offset: $($stripchart[3])"
        }
        else {
            $result = 'Fail'
            $failureDetail = $stripchart
            Write-AzsReadinessLog -Message "Did not receive an expected response from time server '$TimeServer'. $stripchart" -Type 'Error'
        }
    }
    catch {
        $result = 'Fail'
        $failureDetail = $_.Exception.Message
        Write-AzsReadinessLog -Message "Error occurred: '$failureDetail'" -Type 'Error'
    }
    finally {
        $outputObject = @{'Stripchart' = $stripchart; 'NetConnection' = $tnc}
    }

    Write-AzsReadinessLog -Message "Test '$test' finished, result '$result'"
    $hash = @{'Test' = $test; 'Result' = $result; 'FailureDetail' = $failureDetail; 'outputObject' = $outputObject}
    $object = New-Object -TypeName 'PSObject' -Property $hash
    return $object
}

<#
.SYNOPSIS
    Verifies proxy server connectivity and authentication.
.DESCRIPTION
    Attempts to make a web request to the specified proxy server.
#>

function Test-ProxyServer {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.Uri]
        $Proxy,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $ProxyCredential,

        [Parameter(Mandatory = $true)]
        [System.Uri]
        $TestUri
    )

    $test = "Proxy Server $($Proxy.Host)"
    Write-AzsReadinessLog -Message "Starting test '$test'"
    $result = 'OK'

    try {
        Write-AzsReadinessLog -Message "Testing network connection to the proxy server '$Proxy'"
        $tnc = Test-NetConnectionEx -RemoteAddress $Proxy.Host -Port $Proxy.Port

        # Web request to the proxy URL is expected to return 400/401/403 error with details in ErrorDetail.
        # But if ErrorDetails is empty, the proxy couldn't be resolved or didn't respond, so we will throw to the outer catch
        try {
            Write-AzsReadinessLog -Message "Testing HTTP connection to the proxy server '$Proxy'"
            $webResponse = Invoke-WebRequest -Uri $Proxy -UseBasicParsing -TimeoutSec 15
            $proxyOut = $webResponse.BaseResponse
        }
        catch {
            if ($_.ErrorDetails) {
                $proxyOut = @{'Exception' = $_.Exception.Message; 'Details' = $_.ErrorDetails.Message}
            }
            else {
                throw $_
            }
        }

        if (-not $ProxyCredential) {
            Write-AzsReadinessLog -Message "Attempting to access URI '$TestUri' with proxy server '$Proxy'"
        }
        else {
            Write-AzsReadinessLog -Message "Attempting to access URI '$TestUri' with proxy server '$Proxy' and username '$($ProxyCredential.UserName)'"
        }

        $webResponse = Invoke-WebRequest -Uri $TestUri @webParams
        $testOut = $webResponse.BaseResponse
    }
    catch {
        $result = 'Fail'
        $failureDetail = $_.Exception.Message
        Write-AzsReadinessLog -Message "Error while trying to use the proxy server '$Proxy': '$failureDetail'" -Type 'Error'
    }
    finally {
        $outputObject = @{'NetConnection' = $tnc; 'ProxyOutput' = $proxyOut; 'TestUrlOutput' = $testOut}
    }

    Write-AzsReadinessLog -Message "Test '$test' finished, result '$result'"
    $hash = @{'Test' = $test; 'Result' = $result; 'FailureDetail' = $failureDetail; 'outputObject' = $outputObject}
    $object = New-Object -TypeName 'PSObject' -Property $hash
    return $object
}

<#
.SYNOPSIS
    Verifies connectivity to Azure AD Login, Graph, Management Service, and Azure Resource Manager URIs
.DESCRIPTION
    Attempts to make a web request to the Azure endpoints.
#>

function Test-AzureEndpoint {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.String]
        $AzureEnvironment,

        [Parameter()]
        [System.Uri]
        $CustomCloudArmEndpoint,

        [Parameter(Mandatory = $true)]
        [ValidateSet('ARM', 'Graph', 'Login', 'ManagementService')]
        [System.String]
        $EndpointType
    )

    $test = "Azure $EndpointType Endpoint"
    Write-AzsReadinessLog -Message "Starting test '$test'"
    $result = 'OK'
    $outputObject = @{}

    try {
        $armEndpoint = switch ($AzureEnvironment) {
            'AzureCloud'        {'https://management.azure.com/'}
            'AzureUSGovernment' {'https://management.usgovcloudapi.net/'}
            'AzureChinaCloud'   {'https://management.chinacloudapi.cn/'}
            'AzureGermanCloud'  {'https://management.microsoftazure.de/'}
            'CustomCloud'       {$CustomCloudArmEndpoint}
        }

        $armUri = $armEndpoint.TrimEnd('/') + '/metadata/endpoints?api-version=2015-01-01'

        if ($EndpointType -eq 'ARM') {
            Write-AzsReadinessLog -Message "Testing ARM endpoint $armUri"
            $web = Invoke-WebRequestEx -Uri $armUri
            $outputObject = $web

            if ($web.WebResponse.StatusCode -eq 200) {
                Write-AzsReadinessLog -Message "Connection to ARM endpoint successful"
            }
            else {
                throw "Unable to retrieve Azure endpoints from ARM URI at $armEndpoint. Response Status Code = $($web.WebResponse.StatusCode)."
            }
        }
        else {
            Write-AzsReadinessLog -Message "Invoking REST method to retrieve endpoints from '$armUri'"
            $azureEndpoints = Invoke-RestMethod -Uri $armUri @webParams
        }

        $endpoint = switch ($EndpointType) {
            'ARM' {$null}
            'Graph' {$azureEndpoints.graphEndpoint}
            'Login' {$azureEndpoints.authentication.loginEndpoint}
            'ManagementService' {$azureEndpoints.authentication.audiences[0]}
        }

        if ($endpoint) {
            $web = Invoke-WebRequestEx -Uri $endpoint
            $outputObject = $web
        }
        elseif ($EndpointType -ne 'ARM') {
            throw "Unable to retrieve Azure $EndpointType endpoint URL from '$armEndpoint'"
        }

        if ($web.WebResponse.NonHTTPFailure) {
            $result = 'Fail'
            $failureDetail = "Azure $EndpointType endpoint did not respond"
            Write-AzsReadinessLog -Message $failureDetail -Type 'Error'
        }
    }
    catch {
        $result = 'Fail'
        $failureDetail = $_.Exception.Message
        Write-AzsReadinessLog -Message "Error while connecting to Azure endpoint: '$failureDetail'" -Type 'Error'
    }

    Write-AzsReadinessLog -Message "Test '$test' finished, result '$result'"
    $hash = @{'Test' = $test; 'Result' = $result; 'FailureDetail' = $failureDetail; 'outputObject' = $outputObject}
    $object = New-Object -TypeName 'PSObject' -Property $hash
    return $object
}

<#
.SYNOPSIS
    Verifies connectivity to Windows Update Service server URLs
.DESCRIPTION
    Attempts to make a web request to the list of Windows Update or WSUS URLs.
#>

function Test-WindowsUpdateServer {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.Uri]
        $ServerUri
    )

    $test = "Windows Update Server $($ServerUri.Host) port $($ServerUri.Port)"
    Write-AzsReadinessLog -Message "Starting test '$test'"
    $result = 'OK'

    try {
        Write-AzsReadinessLog -Message "Attempting to access URI '$ServerUri'"

        if ($ServerUri.AbsoluteUri.EndsWith('.microsoft.com') -or $ServerUri.AbsoluteUri.EndsWith('.windowsupdate.com')) {
            $web = Invoke-WebRequestEx -Uri $ServerUri
        }
        else {
            $web = Invoke-WebRequestEx -Uri $ServerUri -NoProxy
        }

        $outputObject = $web

        if ($web.WebResponse.NonHTTPFailure) {
            throw "Unable to connect to URI $ServerUri. $($web.WebResponse.Exception)."
        }
}
    catch {
        $result = 'Fail'
        $failureDetail = $_.Exception.Message
        Write-AzsReadinessLog -Message "Error while connecting to Windows Update Server: $failureDetail" -Type 'Error'
    }

    Write-AzsReadinessLog -Message "Test '$test' finished, result '$result'"
    $hash = @{'Test' = $test; 'Result' = $result; 'FailureDetail' = $failureDetail; 'outputObject' = $outputObject}
    $object = New-Object -TypeName 'PSObject' -Property $hash
    return $object
}

<#
.SYNOPSIS
    Verifies that the DNS resource record is present.
.DESCRIPTION
    Checks that the DNS server contains specific resource records required by Azure Stack Edge.
#>

function Test-DnsRegistration {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.String[]]
        $DnsServer,

        [Parameter(Mandatory = $true)]
        [System.String]
        $DnsRecord
    )

    $test = "DNS Registration for $DnsRecord"
    Write-AzsReadinessLog -Message "Starting test '$test'"
    $result = 'OK'

    try {
        Write-AzsReadinessLog -Message "Using DNS servers '$($DnsServer -join ', ')' to resolve name '$DnsRecord'"
        $dnsTest = Resolve-DnsName -Server $DnsServer -Name $DnsRecord -DnsOnly
        Write-AzsReadinessLog -Message "Name '$DnsRecord' resolved to IP addresses $($dnsTest.IPAddress -join ', ')"
    }
    catch {
        $result = 'Fail'
        $failureDetail = $_.Exception.Message
        Write-AzsReadinessLog -Message "Error resolving DNS name: '$failureDetail'" -Type 'Error'
    }
    finally {
        $outputObject = $dnsTest
    }

    Write-AzsReadinessLog -Message "Test '$test' finished, result '$result'"
    $hash = @{'Test' = $test; 'Result' = $result; 'FailureDetail' = $failureDetail; 'outputObject' = $outputObject}
    $object = New-Object -TypeName 'PSObject' -Property $hash
    return $object
}

<#
.SYNOPSIS
    Wrapper function for Test-NetConnection. For internal use.
.DESCRIPTION
    Tests network connection with trace route and TCP test. Writes to readiness log and returns a PSObject with results.
#>

function Test-NetConnectionEx {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.String]
        $RemoteAddress,

        [Parameter()]
        [System.Int32]
        $Port
    )

    $ipAddress = $null
    $silentlyContinue = [System.Management.Automation.ActionPreference]::SilentlyContinue

    if ([System.Net.IPAddress]::TryParse($RemoteAddress, [ref]$ipAddress)) {
        Write-AzsReadinessLog -Message "'$RemoteAddress' is a valid IP address"
    }
    else {
        Write-AzsReadinessLog -Message "'$RemoteAddress' is a DNS name, attempting to resolve"
        $dnsName = Resolve-DnsName -Server $DnsServer -Name $RemoteAddress -DnsOnly
        Write-AzsReadinessLog -Message "'$RemoteAddress' resolved to IP address '$($dnsName.IPAddress -join ', ')'"
    }

    Write-AzsReadinessLog -Message "Tracing route to '$RemoteAddress'"
    $traceRoute = Test-NetConnection -ComputerName $RemoteAddress -TraceRoute -Hops 10 -WarningAction $silentlyContinue

    if ($traceRoute.PingSucceeded) {
        Write-AzsReadinessLog -Message "ICMP test to '$RemoteAddress' succeeded using interface '$($traceRoute.InterfaceAlias)' with source address '$($traceRoute.SourceAddress.IPAddress)'. Route: $($traceRoute.TraceRoute -join ', ')."
    }
    else {
        Write-AzsReadinessLog -Message "ICMP test to '$RemoteAddress' failed using interface '$($traceRoute.InterfaceAlias)' with source address '$($traceRoute.SourceAddress.IPAddress)'. Route: $($traceRoute.TraceRoute -join ', ')." -Type 'Warning'
    }

    if ($Port) {
        Write-AzsReadinessLog -Message "Validating TCP connection to '$RemoteAddress' port $Port"
        $tcpTest = Test-NetConnection -ComputerName $RemoteAddress -Port $Port -WarningAction $silentlyContinue

        if ($tcpTest.TcpTestSucceeded) {
            Write-AzsReadinessLog -Message "TCP connection to '$RemoteAddress' port '$Port' succeeded. Reverse lookup record '$($tcpTest.DnsOnlyRecords.NameHost)'."
        }
        else {
            Write-AzsReadinessLog -Message "TCP connection to '$RemoteAddress' port '$Port' failed" -Type 'Warning'
        }
    }

    $hash = @{
        'ComputerName'     = $traceRoute.ComputerName
        'RemotePort'       = $tcpTest.RemotePort
        'InterfaceAlias'   = $traceRoute.InterfaceAlias
        'PingSucceeded'    = $traceRoute.PingSucceeded
        'TcpTestSucceeded' = $tcpTest.TcpTestSucceeded
        'TraceRoute'       = $traceRoute.TraceRoute
    }
    $object = New-Object -TypeName 'PSObject' -Property $hash
    return $object
}

<#
.SYNOPSIS
    Wrapper function for Invoke-WebRequest. For internal use.
.DESCRIPTION
    Tests network connection with trace route and TCP test, then makes a web request. Writes to readiness log and returns a PSObject with results.
#>

function Invoke-WebRequestEx {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.Uri]
        $Uri,

        [Parameter()]
        [switch]
        $NoProxy
    )

    if (-not $NoProxy -and $webParams.Proxy) {
        Write-AzsReadinessLog -Message "Skipping direct connection test to $($Uri.Host) because proxy server was specified"
        $tnc = "Skipped"
    }
    else {
        Write-AzsReadinessLog -Message "Testing network connection to the host '$($Uri.Host)'"
        $tnc = Test-NetConnectionEx -RemoteAddress $Uri.Host -Port $Uri.Port
    }

    try {
        Write-AzsReadinessLog -Message "Attempting to access web URI '$uri'"

        if (-not $NoProxy -and $webParams.Proxy) {
            $webOut = Invoke-WebRequest -Uri $Uri @webParams
        }
        else {
            $webOut = Invoke-WebRequest -Uri $Uri -TimeoutSec 15 -UseBasicParsing
        }

        $web = $webOut.BaseResponse
    }
    catch {
        $web = @{'Exception' = $_.Exception.Message; 'Details' = $_.ErrorDetails.Message; 'NonHTTPFailure' = [System.String]::IsNullOrEmpty($_.ErrorDetails.Message)}
    }

    $object = New-Object -TypeName 'PSObject' -Property @{'NetConnection' = $tnc; 'WebResponse' = $web}
    return $object
}

<#
.SYNOPSIS
    Parameter validation helper for URI type. For internal use.
.DESCRIPTION
    Tests the parameter is an absolute URI of a supported protocol.
#>

function Test-Uri {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.Uri]
        $Uri,

        [Parameter()]
        [System.String[]]
        $SupportedProtocols = @('http', 'https')
    )

    $result = $true
    Write-AzsReadinessLog -Message "Testing URI '$uri'"

    if (-not $Uri.IsAbsoluteUri) {
        Write-AzsReadinessLog -Message "Invalid URI value: '$Uri' is not a valid URI" -Type 'Error' -ToScreen
        $result = $false
    }
    elseif ($Uri.Scheme -notin $SupportedProtocols) {
        Write-AzsReadinessLog -Message "Invalid protocol for URI: '$($Uri.Scheme)'. Supported protocols are: $($SupportedProtocols -join ', ')." -Type 'Error' -ToScreen
        $result = $false
    }

    return $result
}
# SIG # Begin signature block
# MIIjowYJKoZIhvcNAQcCoIIjlDCCI5ACAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBbCL9TTHAJMF5l
# yjDmQDjrSp/kD3/sZIOtGha2twBrLKCCDYUwggYDMIID66ADAgECAhMzAAABiK9S
# 1rmSbej5AAAAAAGIMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjAwMzA0MTgzOTQ4WhcNMjEwMzAzMTgzOTQ4WjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQCSCNryE+Cewy2m4t/a74wZ7C9YTwv1PyC4BvM/kSWPNs8n0RTe+FvYfU+E9uf0
# t7nYlAzHjK+plif2BhD+NgdhIUQ8sVwWO39tjvQRHjP2//vSvIfmmkRoML1Ihnjs
# 9kQiZQzYRDYYRp9xSQYmRwQjk5hl8/U7RgOiQDitVHaU7BT1MI92lfZRuIIDDYBd
# vXtbclYJMVOwqZtv0O9zQCret6R+fRSGaDNfEEpcILL+D7RV3M4uaJE4Ta6KAOdv
# V+MVaJp1YXFTZPKtpjHO6d9pHQPZiG7NdC6QbnRGmsa48uNQrb6AfmLKDI1Lp31W
# MogTaX5tZf+CZT9PSuvjOCLNAgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUj9RJL9zNrPcL10RZdMQIXZN7MG8w
# VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh
# dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzQ1ODM4NjAfBgNVHSMEGDAW
# gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v
# d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw
# MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov
# L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx
# XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB
# ACnXo8hjp7FeT+H6iQlV3CcGnkSbFvIpKYafgzYCFo3UHY1VHYJVb5jHEO8oG26Q
# qBELmak6MTI+ra3WKMTGhE1sEIlowTcp4IAs8a5wpCh6Vf4Z/bAtIppP3p3gXk2X
# 8UXTc+WxjQYsDkFiSzo/OBa5hkdW1g4EpO43l9mjToBdqEPtIXsZ7Hi1/6y4gK0P
# mMiwG8LMpSn0n/oSHGjrUNBgHJPxgs63Slf58QGBznuXiRaXmfTUDdrvhRocdxIM
# i8nXQwWACMiQzJSRzBP5S2wUq7nMAqjaTbeXhJqD2SFVHdUYlKruvtPSwbnqSRWT
# GI8s4FEXt+TL3w5JnwVZmZkUFoioQDMMjFyaKurdJ6pnzbr1h6QW0R97fWc8xEIz
# LIOiU2rjwWAtlQqFO8KNiykjYGyEf5LyAJKAO+rJd9fsYR+VBauIEQoYmjnUbTXM
# SY2Lf5KMluWlDOGVh8q6XjmBccpaT+8tCfxpaVYPi1ncnwTwaPQvVq8RjWDRB7Pa
# 8ruHgj2HJFi69+hcq7mWx5nTUtzzFa7RSZfE5a1a5AuBmGNRr7f8cNfa01+tiWjV
# Kk1a+gJUBSP0sIxecFbVSXTZ7bqeal45XSDIisZBkWb+83TbXdTGMDSUFKTAdtC+
# r35GfsN8QVy59Hb5ZYzAXczhgRmk7NyE6jD0Ym5TKiW5MIIHejCCBWKgAwIBAgIK
# YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm
# aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw
# OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD
# VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG
# 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la
# UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc
# 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D
# dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+
# lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk
# kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6
# A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd
# X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL
# 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd
# sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3
# T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS
# 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI
# bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL
# BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD
# uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv
# c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf
# MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF
# BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h
# cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA
# YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn
# 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7
# v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b
# pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/
# KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy
# CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp
# mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi
# hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb
# BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS
# oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL
# gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX
# cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCFXQwghVwAgEBMIGVMH4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p
# Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAAGIr1LWuZJt6PkAAAAA
# AYgwDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw
# HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIHqh
# NdeDAnmVlkOg6TIuJB46ijUpSToS+QrnMKU1vfqPMEIGCisGAQQBgjcCAQwxNDAy
# oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20wDQYJKoZIhvcNAQEBBQAEggEAY2Dyo07e8Tu/220NSLoeazu4qI5Ny95HYnuE
# mcHIzocF1ksk+NJl5GTZK0g/Xm7w6iWw9hzjfRnCkYSkfjMuuZStd0v+yfwoy6Vv
# bWttsWUE4rFAHsgaX+s9TnTdC7SDCPYObN/imQ0rwvVIgZ2vPUnjGSccBaU3ZQmr
# Attq8cLEax/B9sLpUE8AKtaoJq15rXOU1WUQsDlyWukW0l3t7XJvHXz96MyXoWcF
# yGwN3zLDArvnNUL4TuAEwcxAd+oL6u//4EzvicVvQNyZ28p9USvumbrgPwjqcX7K
# Jw9wAXwHdHj7NJcYqDiwk2CU1KmGeiNUCyseope4LJF64c16Q6GCEv4wghL6Bgor
# BgEEAYI3AwMBMYIS6jCCEuYGCSqGSIb3DQEHAqCCEtcwghLTAgEDMQ8wDQYJYIZI
# AWUDBAIBBQAwggFZBgsqhkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGE
# WQoDATAxMA0GCWCGSAFlAwQCAQUABCBF/V1/nEvVHbgw7aSbNkZG5f772Zu3K60K
# eWCVPv8ckQIGX90CMRdtGBMyMDIxMDEyMDAzMjYxNy44NTdaMASAAgH0oIHYpIHV
# MIHSMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQL
# EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsT
# HVRoYWxlcyBUU1MgRVNOOkEyNDAtNEI4Mi0xMzBFMSUwIwYDVQQDExxNaWNyb3Nv
# ZnQgVGltZS1TdGFtcCBTZXJ2aWNloIIOTTCCBPkwggPhoAMCAQICEzMAAAE/4X7t
# o9j/v9kAAAAAAT8wDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAg
# UENBIDIwMTAwHhcNMjAxMDE1MTcyODI2WhcNMjIwMTEyMTcyODI2WjCB0jELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9z
# b2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMg
# VFNTIEVTTjpBMjQwLTRCODItMTMwRTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgU2VydmljZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO1F
# 1U5lbR+JB2ejAO7KRMFdLm5arpdDoKyH3TeMURCjsDo7udkW/c0a0xZCt+PCy6Pu
# KGtY1kjArogRhjxEuyEJ368jnB1kbhLaY0DI8UqEMSMV6dzgixF/W2TROg92bsMG
# 4ufWj86pBaC/XlauTNsjYPDCHszV7tt/QOHn0agmPw4X68PyCO8cbgqa9qFV0e02
# EVupE1GnliCP7I+xd0slGYgYOUDJjDtCiR9hYwT47LNiuOcEo0HjosVUeeB2XXn7
# CwTJ1/NiSzeUJQ6NQP8rDTIX7EgAd7w6AM1VamrAiOa/9HcYKtVrFXI2sG+fa4M6
# 6lqjMELX+KbSGcxWDKUCAwEAAaOCARswggEXMB0GA1UdDgQWBBS8OgvMEaMTpYzh
# 66JvyRaMO5iBazAfBgNVHSMEGDAWgBTVYzpcijGQ80N7fEYbxTNoWoVtVTBWBgNV
# HR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2NybC9w
# cm9kdWN0cy9NaWNUaW1TdGFQQ0FfMjAxMC0wNy0wMS5jcmwwWgYIKwYBBQUHAQEE
# TjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpL2Nl
# cnRzL01pY1RpbVN0YVBDQV8yMDEwLTA3LTAxLmNydDAMBgNVHRMBAf8EAjAAMBMG
# A1UdJQQMMAoGCCsGAQUFBwMIMA0GCSqGSIb3DQEBCwUAA4IBAQCfsvpgDi6ueE2N
# lg4gpGBDnQFAmZhPo45rso+R0LqpRn4zonl44FXcmJARMN0r3iDw9subb0N/D0nw
# UbGHWqasQ6wG8DJRYTBxl6Vcr1yBuGhBFWT1PxS8MNG0tXmpBfeZiDBbS/2IdZRt
# TVDV6vMifHeeKOsIRRXLv8tgb0vPtCxxVNEkzYkfPGeR847w88iqaHLd9ofhG3T8
# Ft/c/VVOHQDDa+aXHJJogO+71nKRQXYdU/tLhr4Cqpkh7xQNlsyGZCaNuoMwCKFR
# qRP+kkW1FqORHEhJa4AL6ZYNCiHpUIQaRfuVIw11fM7j7TOQU94hvvOfzk0lLk8I
# Rb6i6KQiMIIGcTCCBFmgAwIBAgIKYQmBKgAAAAAAAjANBgkqhkiG9w0BAQsFADCB
# iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl
# ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMp
# TWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTAwHhcNMTAw
# NzAxMjEzNjU1WhcNMjUwNzAxMjE0NjU1WjB8MQswCQYDVQQGEwJVUzETMBEGA1UE
# CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z
# b2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQ
# Q0EgMjAxMDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKkdDbx3EYo6
# IOz8E5f1+n9plGt0VBDVpQoAgoX77XxoSyxfxcPlYcJ2tz5mK1vwFVMnBDEfQRsa
# lR3OCROOfGEwWbEwRA/xYIiEVEMM1024OAizQt2TrNZzMFcmgqNFDdDq9UeBzb8k
# YDJYYEbyWEeGMoQedGFnkV+BVLHPk0ySwcSmXdFhE24oxhr5hoC732H8RsEnHSRn
# EnIaIYqvS2SJUGKxXf13Hz3wV3WsvYpCTUBR0Q+cBj5nf/VmwAOWRH7v0Ev9buWa
# yrGo8noqCjHw2k4GkbaICDXoeByw6ZnNPOcvRLqn9NxkvaQBwSAJk3jN/LzAyURd
# XhacAQVPIk0CAwEAAaOCAeYwggHiMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQW
# BBTVYzpcijGQ80N7fEYbxTNoWoVtVTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMA
# QTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbL
# j+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1p
# Y3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0w
# Ni0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIz
# LmNydDCBoAYDVR0gAQH/BIGVMIGSMIGPBgkrBgEEAYI3LgMwgYEwPQYIKwYBBQUH
# AgEWMWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9QS0kvZG9jcy9DUFMvZGVmYXVs
# dC5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AUABvAGwAaQBjAHkA
# XwBTAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAAfmiFEN
# 4sbgmD+BcQM9naOhIW+z66bM9TG+zwXiqf76V20ZMLPCxWbJat/15/B4vceoniXj
# +bzta1RXCCtRgkQS+7lTjMz0YBKKdsxAQEGb3FwX/1z5Xhc1mCRWS3TvQhDIr79/
# xn/yN31aPxzymXlKkVIArzgPF/UveYFl2am1a+THzvbKegBvSzBEJCI8z+0DpZaP
# WSm8tv0E4XCfMkon/VWvL/625Y4zu2JfmttXQOnxzplmkIz/amJ/3cVKC5Em4jns
# GUpxY517IW3DnKOiPPp/fZZqkHimbdLhnPkd/DjYlPTGpQqWhqS9nhquBEKDuLWA
# myI4ILUl5WTs9/S/fmNZJQ96LjlXdqJxqgaKD4kWumGnEcua2A5HmoDF0M2n0O99
# g/DhO3EJ3110mCIIYdqwUB5vvfHhAN/nMQekkzr3ZUd46PioSKv33nJ+YWtvd6mB
# y6cJrDm77MbL2IK0cs0d9LiFAR6A+xuJKlQ5slvayA1VmXqHczsI5pgt6o3gMy4S
# KfXAL1QnIffIrE7aKLixqduWsqdCosnPGUFN4Ib5KpqjEWYw07t0MkvfY3v1mYov
# G8chr1m1rtxEPJdQcdeh0sVV42neV8HR3jDA/czmTfsNv11P6Z0eGTgvvM9YBS7v
# DaBQNdrvCScc1bN+NR4Iuto229Nfj950iEkSoYIC1zCCAkACAQEwggEAoYHYpIHV
# MIHSMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQL
# EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsT
# HVRoYWxlcyBUU1MgRVNOOkEyNDAtNEI4Mi0xMzBFMSUwIwYDVQQDExxNaWNyb3Nv
# ZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQBO1zQhf9bHJcmA
# oCSPgmjIAjoiGKCBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw
# MA0GCSqGSIb3DQEBBQUAAgUA47GwFDAiGA8yMDIxMDEyMDAzMjMwMFoYDzIwMjEw
# MTIxMDMyMzAwWjB3MD0GCisGAQQBhFkKBAExLzAtMAoCBQDjsbAUAgEAMAoCAQAC
# AhPnAgH/MAcCAQACAhGcMAoCBQDjswGUAgEAMDYGCisGAQQBhFkKBAIxKDAmMAwG
# CisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZIhvcNAQEF
# BQADgYEAnFQYfoip46lSlBtqbKkc4ndQDFOlEAF+NQG6uvCQoV+2b/J3pvHwNGt0
# 4f8aMRx4CooyOxOv5lbcXJXTKjrUvRHKlPKcNxRMzclJFX2O5REIZTF6s12QbST8
# brPfpuw7FQURVZ1zZ8d4D+H5fWjQU3drZb9CQ9NmYJlfpCyh9Z4xggMNMIIDCQIB
# ATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAT/hfu2j2P+/
# 2QAAAAABPzANBglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3
# DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCDmgGiQWI0DPDmUYMJO3nQRSwdAthx/it2P
# 6A/JjOxTZjCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIEQ7Wa+cFXjB3Kah
# 2rWUmT7Mm8Jq+UIIjLxbPiMZKUsgMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzAR
# BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p
# Y3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3Rh
# bXAgUENBIDIwMTACEzMAAAE/4X7to9j/v9kAAAAAAT8wIgQgjnHalZ7An9RTEJCJ
# wfJejDqNvv13Ckzl5o7CawgOfm4wDQYJKoZIhvcNAQELBQAEggEAM5YPD9cdocKL
# iyXu9tNuKV52nsj3XBG1x7d+bV7QzOM65Iu4T+PvJCR+W8yibAVWA/+Z/wLSn6Jo
# uGyGgCSkRxl0W2cSLh8jhgZlvoJunJf2EsTaBCRe94FSq9fRkl1P0aIZ1NeHWReU
# 65gwvqW9+V0tHCqhCvvG/ZanQDQXuThY8wCnUowC+OSjnKOs3Fgau5oiThk3x8+U
# lXQGbHGbIh0fLli38XI+Kd2ZBOjw1D47aLWAWjPZch67JomDI6f0K7oUdybuE7U6
# uZN/MjI+gO6RkYuccVPWM99BjRSqOAuGgMRO0XY+YO9hjq2R6Eq4lZvV62Czd5k2
# RHAyu5fW3Q==
# SIG # End signature block