NetworkValidation/Microsoft.AzureStack.ReadinessChecker.NetworkValidation.psm1
<#############################################################
# # # Copyright (C) Microsoft Corporation. All rights reserved. # # # #############################################################> $global:ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop $global:ProgressPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue Import-Module -Name $PSScriptRoot\..\Microsoft.AzureStack.ReadinessChecker.Reporting.psm1 -Force <# .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. #> 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', 'BorderUplink', 'IPConfig', 'BgpPeering', 'BgpDefaultRoute', 'DnsServer', 'TimeServer', 'SyslogServer', 'Proxy', 'AzureEndpoint', 'AdfsEndpoint', 'CustomUrl', 'WindowsUpdateServer', 'DuplicateIP', 'DnsRegistration' )] [System.String[]] $RunTests, # List of test to skip. Default is to run all tests. [Parameter()] [ValidateSet( 'BorderUplink', 'IPConfig', 'BgpPeering', 'BgpDefaultRoute', 'DnsServer', 'TimeServer', 'SyslogServer', 'AzureEndpoint', 'AdfsEndpoint', 'WindowsUpdateServer', 'DuplicateIP', 'DnsRegistration' )] [System.String[]] $SkipTests, # Path to the virtual TOR image. [Parameter(ParameterSetName = 'Hub')] [System.String] $TorImagePath, # 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(ParameterSetName = 'Edge')] [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', # List of additional URLs to test [Parameter()] [System.Uri[]] $CustomUrl, # 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' ), # 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 $Global:OutputPath = $OutputPath Write-Header -Invocation $MyInvocation -Params $PSBoundParameters $readinessReport = Get-AzsReadinessProgress -Clean:$CleanReport $readinessReport = Add-AzsReadinessCheckerJob -Report $readinessReport $u = 'admin' $p = 'YourPaSsWoRd' $factoryCred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($u, (ConvertTo-SecureString -String $p -AsPlainText -Force)) $defaultHubTests = @( 'LinkLayer' 'BorderUplink' 'IPConfig' 'BgpPeering' 'BgpDefaultRoute' 'DnsServer' 'TimeServer' 'SyslogServer' 'AzureEndpoint' 'AdfsEndpoint' 'DuplicateIP' ) $defaultEdgeTests = @( 'LinkLayer' 'IPConfig' 'DnsServer' 'TimeServer' 'AzureEndpoint' 'WindowsUpdateServer' 'DuplicateIP' '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' } if ($CustomUrl) { $RunTests += 'CustomUrl' } Write-AzsReadinessLog -Message "Preparing to run tests: $($RunTests -join ', ')" #endregion #region ParameterValidation if ($RunTests -contains 'BorderUplink') { $layer3 = $true Write-AzsReadinessLog -Message "Validating prerequisites" if ((Get-WindowsFeature | Where-Object {$_.Name -in 'Hyper-V', 'Hyper-V-PowerShell' -and $_.Installed}).Count -eq 2) { Write-AzsReadinessLog -Message 'Hyper-V features are installed' } else { Write-AzsReadinessLog -Message 'Required Hyper-V features are not installed' -Type 'Error' -ToScreen $paramValidation = $false } if (-not (Get-Module -ListAvailable -Name 'Posh-SSH')) { Write-AzsReadinessLog -Message 'Posh-SSH PowerShell module is not installed' -Type 'Error' -ToScreen $paramValidation = $false } if (-not (Test-Path -Path $TorImagePath -PathType Leaf)) { Write-AzsReadinessLog -Message 'Path to TOR image not found. Provide the correct value to the -TorImagePath parameter.' -Type 'Error' -ToScreen $paramValidation = $false } } 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 $syslogServer = $deploymentData.ConfigData.InputData.Cloud.SyslogServerIPv4Address $switchData = $deploymentData.ConfigData.EnvironmentData.Switch $ipConfigData = $deploymentData.ConfigData.IPConfigData $p2pNetworks = $ipConfigData | Where-Object {$_.Name -like "P2P_Rack00/B?_To_Rack??/*"} $loopbackNetworks = $ipConfigData | Where-Object {$_.Name -like "Loopback_Rack??/Tor?"} $switchMgmtNetworks = $ipConfigData | Where-Object {$_.Name -like "Rack??-SwitchMgmt"} $bmcNetwork = $ipConfigData | Where-Object {$_.Name -eq "Rack01-BmcMgmt"} $externalNetwork = $ipConfigData | Where-Object {$_.Name -eq "CL01-External-VIPS"} $skipDupTest = ($p2pNetworks.Assignments | Where-Object {$_.Name -like 'Rack??/*'}).IPv4Address $skipDupTest += ($loopbackNetworks.Assignments).IPv4Address $skipDupTest += ($switchMgmtNetworks.Assignments).IPv4Address $skipDupTest += ($bmcNetwork.Assignments | Where-Object {$_.Name -like '*DVM*' -or $_.Name -eq 'Gateway'}).IPv4Address $skipDupTest += $externalNetwork.IPv4Gateway $skipDupTest += $externalNetwork.IPv4FirstAddress $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') { Write-AzsReadinessLog -Message 'Azure Endpoint test will be skipped because this environment uses ADFS identity provider' $adfsEndpoint = $deploymentData.ScaleUnits.DeploymentData.AdfsMetadataUri $RunTests = $RunTests -ne 'AzureEndpoint' } else { Write-AzsReadinessLog -Message 'ADFS test will be skipped because this environment uses Azure AD identity provider' $AzureEnvironment = $deploymentData.ScaleUnits.DeploymentData.InfraAzureEnvironment $RunTests = $RunTests -ne 'AdfsEndpoint' } if ($deploymentData.ConfigData.InputData.BorderConnectivity -ne 'BGP Routing') { Write-AzsReadinessLog -Message 'BGP tests will be skipped because this environment uses static routing' $RunTests = $RunTests -ne 'BgpPeering' $RunTests = $RunTests -ne 'BgpDefaultRoute' } } 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 } } # Some Azure Stack Hub tests only apply in layer-3 scenarios if ($solutionType -eq 'Hub') { if ($layer3) { $RunTests = $RunTests -ne 'IPConfig' } else { $RunTests = $RunTests -ne 'BgpPeering' $RunTests = $RunTests -ne 'BgpDefaultRoute' } } 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 'SyslogServer') { if ($syslogServer) { Write-AzsReadinessLog -Message "Validating parameter 'SyslogServer' value '$syslogServer'" $parseAddress = $null if ([System.Net.IPAddress]::TryParse($syslogServer, [ref]$parseAddress)) { Write-AzsReadinessLog -Message "'$syslogServer' is a valid IP address" } else { Write-AzsReadinessLog -Message "'$syslogServer' is not a valid IP address" -Type 'Error' -ToScreen $paramValidation = $false } } else { Write-AzsReadinessLog -Message 'Parameter Syslog Server is not specified in the deployment configuration file. The Syslog Server test will be skipped.' $RunTests = $RunTests -ne 'SyslogServer' } } 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 'AdfsEndpoint') { Write-AzsReadinessLog -Message "Validating parameter 'AdfsEndpoint' value '$adfsEndpoint'" if (-not $adfsEndpoint.EndsWith('.xml')) { Write-AzsReadinessLog -Message "ADFS metadata URI '$adfsEndpoint' does not include the path to metadata.xml. Appending the default path." $adfsEndpoint = $adfsEndpoint.TrimEnd('/') + '/FederationMetadata/2007-06/FederationMetadata.xml' Write-AzsReadinessLog -Message "New ADFS metadata URI is '$adfsEndpoint'." } $paramValidation = $paramValidation -and (Test-Uri -Uri $adfsEndpoint -SupportedProtocols 'https') } if ($RunTests -contains 'CustomUrl') { Write-AzsReadinessLog -Message "Validating parameter 'CustomUrl' value '$($CustomUrl -join ', ')'" foreach ($uri in $CustomUrl) { Write-AzsReadinessLog -Message "Parsing URL value '$uri'" $paramValidation = $paramValidation -and (Test-Uri -Uri $uri) } } 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' } Write-AzsReadinessLog -Message "The following tests will be executed: $($RunTests -join ', ')" -ToScreen #endregion #region Main Write-AzsReadinessLog -Message "Validating Azure Stack $solutionType Network Readiness" -ToScreen $result = @() $helpMessage = @() 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') { $helpMessage += "$($testResult.Test): $($testResult.FailureDetail)" throw 'No network connection' } } if ($RunTests -contains 'BorderUplink') { Write-AzsReadinessLog -Message "Starting border uplink discovery. This operation will take a few minutes." -ToScreen $testResult = Test-BorderUplink -Configuration $p2pNetworks Write-AzsResult -In $testResult $result += $testResult $borderUplinks = $testResult.OutputObject | Where-Object {$_.PeerConnected} $torIpAssigned = $borderUplinks | Where-Object {$_.WasDiscovered} # If the border uplink test fails, no other tests are applicable if ($testResult.Result -eq 'OK') { Write-AzsReadinessLog -Message "Starting the virtual router for all subsequent tests. This operation will take a few minutes." -ToScreen $torSession = Start-VirtualTor -ImagePath $TorImagePath -Credential $factoryCred -Configuration $ipConfigData -DnsServer $DnsServer if (-not $torSession.Connected) { throw 'Failed to start the virtual TOR. Check the AzsReadinessChecker log file for details.' } } else { $helpMessage += "$($testResult.Test): $($testResult.FailureDetail)" throw 'Border connectivity test failed' } } 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') { $helpMessage += "$($testResult.Test): $($testResult.FailureDetail)" throw 'Invalid IP configuration' } } # If more than one interface is connected, all connectivity tests are performed from each interface, with all other interfaces shut down if ($layer3) { $uplinks = $borderUplinks } else { $uplinks = @('Layer-2') } $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 ($uplink in $uplinks) { if ($layer3) { $localAssignment = $uplink.Subnet.Assignments | Where-Object {$_.Offset -eq 2} $remoteAssignment = $uplink.Subnet.Assignments | Where-Object {$_.Offset -eq 1} $localAsn = ($switchData | Where-Object {$_.Type -eq 'TOR1'}).Asn $remoteAsn = ($switchData | Where-Object {$_.Type -eq 'Border'} | Select-Object -First 1).Asn $loopbackAddress = ($loopbackNetworks.Assignments | Where-Object {$_.Name -eq $localAssignment.Name}).IPv4Address Write-AzsReadinessLog -Message "UPLINK: '$($uplink.Subnet.Name)', Local IP: '$($localAssignment.IPv4Address)', Remote IP: '$($remoteAssignment.IPv4Address)', Interface: '$($uplink.Netadapter.Name)'" -ToScreen Write-AzsReadinessLog -Message "Connecting network adapter '$($uplink.Netadapter.Name)' to the virtual router" Connect-VirtualTorUplink -NetAdapterInterfaceDescription $uplink.NetAdapter.InterfaceDescription Write-AzsReadinessLog -Message "Applying interface-specific configuration to the virtual TOR" # TODO: Add static routing support Set-TorConfig ` -SSHSession $torSession ` -Credential $factoryCred ` -Clear ` -LocalAsn $localAsn ` -LoopbackAddress $loopbackAddress ` -LocalAddress $localAssignment.IPv4Address ` -PrefixLength $uplink.Subnet.IPv4MaskBits ` -PeerAddress $remoteAssignment.IPv4Address ` -RemoteAsn $remoteAsn ` -BmcNetworkAddress $bmcNetwork.IPv4Gateway ` -BmcNetworkPrefixLength $bmcNetwork.IPv4MaskBits ` -ExternalNetworkAddress $externalNetwork.IPv4Gateway ` -ExternalNetworkPrefixLength $externalNetwork.IPv4MaskBits if ($RunTests -contains 'BgpPeering') { $testResult = Test-BgpPeering -SSHSession $torSession -Credential $factoryCred Write-AzsResult -In $testResult $result += $testResult if ($testResult.Result -ne 'OK') { $helpMessage += "$($testResult.Test): $($testResult.FailureDetail)" Write-AzsReadinessLog -Message "Skipping other tests for this uplink as not applicable" continue } } if ($RunTests -contains 'BgpDefaultRoute') { $testResult = Test-BgpDefaultRoute -SSHSession $torSession -Credential $factoryCred Write-AzsResult -In $testResult $result += $testResult if ($testResult.Result -ne 'OK') { $helpMessage += "$($testResult.Test): $($testResult.FailureDetail)" Write-AzsReadinessLog -Message "Skipping other tests for this uplink as not applicable" continue } } } foreach ($netAdapter in $netAdapters) { if ($netAdapters.Count -gt 1) { Write-AzsReadinessLog -Message "Using network adapter '$($netAdapter.Name)'" -ToScreen } foreach ($otherNetAdapter in $netAdapters | Where-Object {$_.InterfaceIndex -ne $netAdapter.InterfaceIndex}) { Write-AzsReadinessLog -Message "Disabling network adapter name '$($otherNetAdapter.Name)', description '$($otherNetAdapter.InterfaceDescription)'" Disable-NetAdapter -Name $otherNetAdapter.Name -Confirm:$false $disabledNetAdapters += $otherNetAdapter.Name } if ($RunTests -contains 'DnsServer') { foreach ($serverAddress in $DnsServer) { $testResult = Test-DnsServer -DnsServer $serverAddress -ResolveName $DnsName Write-AzsResult -In $testResult $result += $testResult if ($testResult.Result -ne 'OK') { $helpMessage += "$($testResult.Test): $($testResult.FailureDetail)" } } } if ($RunTests -contains 'TimeServer') { foreach ($serverAddress in $TimeServer) { $testResult = Test-TimeServer -TimeServer $serverAddress Write-AzsResult -In $testResult $result += $testResult if ($testResult.Result -ne 'OK') { $helpMessage += "$($testResult.Test): $($testResult.FailureDetail)" } } } if ($RunTests -contains 'SyslogServer') { $testResult = Test-SyslogServer -SyslogServer $syslogServer Write-AzsResult -In $testResult $result += $testResult if ($testResult.Result -ne 'OK') { $helpMessage += "$($testResult.Test): $($testResult.FailureDetail)" } } if ($RunTests -contains 'Proxy') { $testResult = Test-ProxyServer -Proxy $Proxy -ProxyCredential $ProxyCredential -TestUri $ExternalUri Write-AzsResult -In $testResult $result += $testResult if ($testResult.Result -ne 'OK') { $helpMessage += "$($testResult.Test): $($testResult.FailureDetail)" } } 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 ($testResult.Result -ne 'OK') { $helpMessage += "$($testResult.Test): $($testResult.FailureDetail)" } } } if ($RunTests -contains 'AdfsEndpoint') { $testResult = Test-AdfsEndpoint -MetadataUri $adfsEndpoint Write-AzsResult -In $testResult $result += $testResult if ($testResult.Result -ne 'OK') { $helpMessage += "$($testResult.Test): $($testResult.FailureDetail)" } } if ($RunTests -contains 'CustomUrl') { foreach ($serverAddress in $CustomUrl) { $testResult = Test-CustomUrl -ServerUri $serverAddress Write-AzsResult -In $testResult $result += $testResult if ($testResult.Result -ne 'OK') { $helpMessage += "$($testResult.Test): $($testResult.FailureDetail)" } } } if ($RunTests -contains 'WindowsUpdateServer') { foreach ($serverAddress in $WindowsUpdateServer) { $testResult = Test-WindowsUpdateServer -ServerUri $serverAddress Write-AzsResult -In $testResult $result += $testResult if ($testResult.Result -ne 'OK') { $helpMessage += "$($testResult.Test): $($testResult.FailureDetail)" } } } if ($disabledNetAdapters) { Write-AzsReadinessLog -Message "Re-enabling previously disabled network adapters" Enable-NetAdapter -Name $disabledNetAdapters Start-Sleep -Seconds 5 $disabledNetAdapters = @() } } } if ($netAdapters.Count -gt 1) { Write-AzsReadinessLog -Message "Testing network services configuration" -ToScreen } if ($RunTests -contains 'DuplicateIP') { $testResult = Test-DuplicateIP -AddressRanges $addressRanges -SkipAddresses $skipDupTest Write-AzsResult -In $testResult $result += $testResult if ($testResult.Result -ne 'OK') { $helpMessage += "$($testResult.Test): $($testResult.FailureDetail)" } } 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 if ($testResult.Result -ne 'OK') { $helpMessage += "$($testResult.Test): $($testResult.FailureDetail)" } } } #endregion } catch { Write-AzsReadinessLog -Message "Network validation 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 } if ($layer3) { $null = Stop-VirtualTor Start-Sleep -Seconds 5 } foreach ($ipAssigned in $torIpAssigned) { $ip = ($ipAssigned.Subnet.Assignments | Where-Object {$_.Offset -eq 2}).IPv4Address $netAdapter = $ipAssigned.NetAdapter Write-AzsReadinessLog -Message "Removing previously assigned IP address $ip from the interface $($netAdapter.Name)" Remove-NetIPAddress -InterfaceIndex $netAdapter.InterfaceIndex -IPAddress $ip -Confirm:$false } # Write results to readiness report Write-AzsResult -In $helpMessage -AdditionalHelpUrl 'http://aka.ms/azsnrc' $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' 'NetworkAddresses' ) 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)'" } try { $vNics = Get-VMNetworkAdapter -ManagementOS } catch {} if ($connectedEthernet = @($netAdapters | Where-Object {$_.MediaType -eq '802.3' -and $_.Status -eq 'Up' -and $_.NetworkAddresses -notin $vNics.MacAddress})) { $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 border L3 connectivity. .DESCRIPTION Lists all connected physical network adapters with default IP configuration. For each NIC, attempt to assign a P2P IP address and ping the peer address. #> function Test-BorderUplink { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Object[]] $Configuration ) $test = 'Border Uplink' Write-AzsReadinessLog -Message "Starting test '$test'" $netAdapterProperties = @( 'Name' 'InterfaceDescription' 'InterfaceIndex' 'MacAddress' 'MediaType' 'PhysicalMediaType' 'Status' 'AdminStatus' 'LinkSpeed' 'MediaConnectionState' 'ConnectorPresent' 'DriverInformation' 'DriverFileName' 'NetworkAddresses' ) try { $outputObject = @() $matchedSubnets = @() $assignedIP = @{} $torIPs = ($Configuration.Assignments | Where-Object {$_.Offset -eq 2}).IPv4Address $netAdapters = Get-NetAdapter | Select-Object -Property $netAdapterProperties $vNics = Get-VMNetworkAdapter -ManagementOS $pNics = $netAdapters | Where-Object {$_.MediaType -eq '802.3' -and $_.Status -eq 'Up' -and $_.NetworkAddresses -notin $vNics.MacAddress} foreach ($nic in $pNics) { Write-AzsReadinessLog -Message "Testing network adapter '$($nic.Name)'" $ping = $false # Determine whether to use the currently assigned P2P IP, discover a P2P IP, or skip the interface if ($ipAddress = Get-NetIPAddress -AddressFamily IPv4 | Where-Object {$_.InterfaceIndex -eq $nic.InterfaceIndex} | Select-Object -First 1) { $ip = $ipAddress.IPAddress Write-AzsReadinessLog -Message "Interface IP address: $ip" if ($ipAddress.PrefixOrigin -in 'Manual', 'Dhcp') { Write-AzsReadinessLog -Message "Interface has a $($ipAddress.PrefixOrigin)-assigned IP address" if ($ip -in $torIPs) { Write-AzsReadinessLog -Message "Interface has a TOR IP address $ip assigned" $discover = $false } else { Write-AzsReadinessLog -Message "The interface's IP address is not associated with TOR configuration and will be skipped" continue } } else { Write-AzsReadinessLog -Message "The interface doesn't have a valid IP address assigned. Will use this interface for uplink discovery" $discover = $true } } else { Write-AzsReadinessLog -Message "Interface does not have IP protocol enabled. It could be attached to a Hyper-V virtual switch" continue } # If a P2P IP was previously assigned, ping the peer, otherwise iteratively assign IP addresses and try to ping the respective peer until found if ($discover) { foreach ($p2pSubnet in $Configuration | Where-Object {$_.Name -notin $matchedSubnets.Name}) { $ip = ($p2pSubnet.Assignments | Where-Object {$_.Offset -eq 2}).IPv4Address $peerIp = ($p2pSubnet.Assignments | Where-Object {$_.Offset -eq 1}).IPv4Address Write-AzsReadinessLog -Message "Assigning IP address $ip to the interface $($nic.Name) and attempting discovery" $null = New-NetIPAddress -InterfaceIndex $nic.InterfaceIndex -IPAddress $ip -PrefixLength $p2pSubnet.IPv4MaskBits $assignedIP.Add($nic.InterfaceIndex, $ip) Start-Sleep -Seconds 5 Write-AzsReadinessLog -Message "Attempting to ping remote address $peerIp" if (Test-Connection -ComputerName $peerIp -Count 5 -Quiet) { Write-AzsReadinessLog -Message "Ping successful" $ping = $true $matchedSubnets += $p2pSubnet break } else { Write-AzsReadinessLog -Message "Peer $peerIp did not respond" Write-AzsReadinessLog -Message "Removing IP address $ip from the interface $($nic.Name)" Remove-NetIPAddress -InterfaceIndex $nic.InterfaceIndex -IPAddress $ip -Confirm:$false $assignedIP.Remove($nic.InterfaceIndex) Start-Sleep -Seconds 5 } } } else { $p2pSubnet = $Configuration | Where-Object {$_.Assignments.IPv4Address -contains $ip} $matchedSubnets += $p2pSubnet $peerIp = ($p2pSubnet.Assignments | Where-Object {$_.Offset -eq 1}).IPv4Address Write-AzsReadinessLog -Message "Attemping to ping remote address $peerIp" if (Test-Connection -ComputerName $peerIp -Count 5 -Quiet) { Write-AzsReadinessLog -Message "Ping successful" $ping = $true } else { Write-AzsReadinessLog -Message "Peer $peerIp did not respond" } } $outputObject += @{NetAdapter = $nic; Subnet = $p2pSubnet; PeerConnected = $ping; WasDiscovered = $discover} } } catch { $result = 'Fail' $failureDetail = $_.Exception.Message Write-AzsReadinessLog -Message "Test failed with error: $failureDetail" -Type 'Error' foreach ($interfaceIndex in $assignedIP.Keys) { $ip = $assignedIP.$interfaceIndex Write-AzsReadinessLog -Message "Removing IP address $ip from the interface $interfaceIndex" Remove-NetIPAddress -InterfaceIndex $interfaceIndex -IPAddress $ip -Confirm:$false Start-Sleep -Seconds 5 } } if ($outputObject | Where-Object {$_.PeerConnected}) { $result = 'OK' } else { $result = 'Fail' } 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[]] $SkipAddresses ) $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} $ipRoutes = Get-NetRoute -AddressFamily IPv4 | Where-Object {$_.NextHop -ne '0.0.0.0'} $gatewayNeighbor = $neighbors | Where-Object {$_.IPAddress -in $ipRoutes.NextHop} foreach ($neighbor in $neighbors) { if ($neighbor.IPAddress -in $addressList) { if ($neighbor.LinkLayerAddress -in $gatewayNeighbor.LinkLayerAddress) { Write-AzsReadinessLog -Message "IP Address '$($neighbor.ipAddress)' resolves to the gateway MAC address. Proxy ARP is enabled on the network. Check that the IP address is not in use." -Type 'Warning' $failureDetail = 'Some IP addresses allocated to Azure Stack may be active on the network. Check the output log for more details.' if ($result -eq 'OK') { $result = 'WARNING' } } else { Write-AzsReadinessLog -Message "IP Address '$($neighbor.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 that the BGP peering has established. .DESCRIPTION Communicates with the TOR over SSH to determine if the BGP peering has successfully established. #> function Test-BgpPeering { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SSH.SshSession] $SSHSession, [Parameter(Mandatory = $true)] [System.Management.Automation.PSCredential] $Credential, [Parameter()] [System.Int32] $Timeout = 60 ) $test = 'BGP Peering' Write-AzsReadinessLog -Message "Starting test '$test'" try { $result = 'Fail' $stopwatch = New-Object -TypeName System.Diagnostics.Stopwatch $stopwatch.Start() do { Write-AzsReadinessLog -Message "Retrieving BGP peering status from the TOR" $bgpStatus = Invoke-TorCommand -SSHSession $SSHSession -Credential $Credential -Command 'show ip bgp summary' $peerStatusArray = $bgpStatus[11] -split ' ' | Where-Object {$_} $peerStatusString = $peerStatusArray[8] if ($peerStatusString) { Write-AzsReadinessLog -Message "Retrieved peer uptime status from the TOR: '$peerStatusString'" if ($peerStatusString -ne 'never') { Write-AzsReadinessLog -Message "BGP peer connection established" $result = 'OK' } else { Start-Sleep -Seconds 5 } } else { Write-AzsReadinessLog -Message "BGP status query did not return the expected output. Inspect the log for more information." -Type 'Warning' Start-Sleep -Seconds 5 } } until (($result -eq 'OK') -or ($stopwatch.Elapsed.TotalSeconds -ge $Timeout)) } 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' = $peerStatusString} $object = New-Object -TypeName 'PSObject' -Property $hash return $object } <# .SYNOPSIS Verifies that the BGP peer is advertising the default route 0.0.0.0/0. .DESCRIPTION Communicates with the TOR over SSH to determine if the BGP default route is advertised. #> function Test-BgpDefaultRoute { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [SSH.SshSession] $SSHSession, [Parameter(Mandatory = $true)] [System.Management.Automation.PSCredential] $Credential, [Parameter()] [System.Int32] $Timeout = 60 ) $test = 'BGP Default Route' Write-AzsReadinessLog -Message "Starting test '$test'" try { $result = 'Fail' $stopwatch = New-Object -TypeName System.Diagnostics.Stopwatch $stopwatch.Start() do { Write-AzsReadinessLog -Message "Retrieving BGP routes from the TOR" $bgpNetwork = Invoke-TorCommand -SSHSession $SSHSession -Credential $Credential -Command 'show ip bgp network' $bgpRoutesString = $bgpNetwork | Where-Object {$_ -like '?>*' -or $_ -like '?=*'} $bgpRoutes = @() foreach ($entry in $bgpRoutesString) { $entryData = $entry -split ' ' | Where-Object {$_} $bgpRoutes += [PSCustomObject]@{ Network = $entryData[1] NextHop = $entryData[2] Metric = $entryData[3] Weight = $entryData[4] Path = $entryData[5] } } if ($bgpRoutes | Where-Object {$_.Network -eq '0.0.0.0/0'}) { Write-AzsReadinessLog -Message "Default route is advertised" $result = 'OK' } else { Start-Sleep -Seconds 5 } } until (($result -eq 'OK') -or ($stopwatch.Elapsed.TotalSeconds -ge $Timeout)) } 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' = $bgpRoutes} $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 | Select-String -Pattern 'error' 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 syslog server connectivity. .DESCRIPTION Checks that the syslog server can be reached over ICMP. Checks that the syslog server can be reached over TCP port 514. #> function Test-SyslogServer { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $SyslogServer ) $test = "Syslog Server $SyslogServer" Write-AzsReadinessLog -Message "Starting test '$test'" try { Write-AzsReadinessLog -Message "Testing Syslog server '$SyslogServer'" $tnc = Test-NetConnectionEx -RemoteAddress $SyslogServer -Port 514 if ($tnc.PingSucceeded -and $tnc.TcpTestSucceeded) { Write-AzsReadinessLog -Message "Syslog server $SyslogServer is reachable on TCP port 514" $result = 'OK' } else { Write-AzsReadinessLog -Message "Syslog server $SyslogServer ping result is '$($tnc.PingSucceeded)' and TCP test result is '$($tnc.TcpTestSucceeded)'." -Type 'Error' $result = 'Fail' } } catch { $result = 'Fail' $failureDetail = $_.Exception.Message Write-AzsReadinessLog -Message "Error while testing connection: '$failureDetail'" -Type 'Error' } finally { $outputObject = $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' 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 the ADFS server .DESCRIPTION Reads the federation metadata from the ADFS server specified in deployment data #> function Test-AdfsEndpoint { [CmdletBinding()] param ( [Parameter()] [System.Uri] $MetadataUri ) $test = "ADFS Metadata Endpoint" Write-AzsReadinessLog -Message "Starting test '$test'" $result = 'OK' $failureDetail = @() try { $sslSetting = [Net.ServicePointManager]::SecurityProtocol Write-AzsReadinessLog -Message 'Forcing TLS 1.2 security' [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Write-AzsReadinessLog -Message "Retrieving ADFS metadata from '$MetadataUri'" $web = Invoke-WebRequestEx -Uri $MetadataUri -NoProxy if ($web.WebResponse.StatusCode -eq 200) { Write-AzsReadinessLog -Message "Successfully downloaded the metdata file" } else { throw "Unable to retrieve ADFS metadata. Response Status Code = $($web.WebResponse.StatusCode)." } Write-AzsReadinessLog -Message "Parsing ADFS metadata" [System.Xml.XmlDocument]$xml = $web.Content $outputObject = $xml if (-not $xml.EntityDescriptor) { $failureDetail += 'Entity Descriptor missing from federation metadata' } if (-not $xml.EntityDescriptor.EntityId) { $failureDetail += 'Entity Id in Entity Descriptor missing from federation metadata' } if (-not $xml.EntityDescriptor.RoleDescriptor) { $failureDetail += 'Role Descriptor missing from federation metadata' } if (-not $xml.EntityDescriptor.Signature) { $failureDetail += 'No signature in federation metadata file' } if ($failureDetail) { $result = 'Fail' } } catch { $result = 'Fail' $failureDetail = $_.Exception.Message Write-AzsReadinessLog -Message "Error during ADFS endpoint validation: '$failureDetail'" -Type 'Error' } finally { Write-AzsReadinessLog -Message 'Restoring previous SSL settings' [Net.ServicePointManager]::SecurityProtocol = $sslSetting } 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 connectivity to any user-specified URL .DESCRIPTION Attempts to make a web request to the provided URL. #> function Test-CustomUrl { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Uri] $ServerUri ) $test = "URL $ServerUri" Write-AzsReadinessLog -Message "Starting test '$test'" $result = 'OK' try { Write-AzsReadinessLog -Message "Attempting to access URI '$ServerUri'" $web = Invoke-WebRequestEx -Uri $ServerUri $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 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 { $DnsRecord = $DnsRecord.Replace('*', 'testname') 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-Connection. .DESCRIPTION Pings a remote computer for a number of times, returning immediately after the first successful ping. #> function Test-ConnectionEx { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [System.String] $RemoteAddress, [Parameter()] [System.Int32] $Count ) for ($i = 0; $i -le $Count; $i++) { Write-AzsReadinessLog -Message "Pinging $RemoteAddress" if (Test-Connection -ComputerName $RemoteAddress -Count 1 -Quiet) { return $true } } return $false } <# .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 $content = $webOut.Content } catch { $web = @{'Exception' = $_.Exception.Message; 'Details' = $_.ErrorDetails.Message; 'NonHTTPFailure' = [System.String]::IsNullOrEmpty($_.ErrorDetails.Message)} } $object = New-Object -TypeName 'PSObject' -Property @{'NetConnection' = $tnc; 'WebResponse' = $web; 'Content' = $content} 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 } <# .SYNOPSIS Returns an IP address when provided a subnet address and an offset. For internal use. #> function Get-IPAddressOffset { [CmdletBinding()] [OutputType([System.Net.IPAddress])] param ( [Parameter(Mandatory = $true)] [System.Net.IPAddress] $SubnetAddress, [Parameter(Mandatory = $true)] [System.Int32] $Offset ) $ipInt = ([System.Net.IPAddress][System.String]([System.Net.IPAddress]$SubnetAddress).Address).Address $ipInt = $ipInt + $Offset $result = [System.Net.IPAddress][System.String]$ipInt return $result } <# .SYNOPSIS Prepare the NRC host for running Layer 3 tests. .DESCRIPTION Configure Hyper-V environment, provision SONiC virtual machines and apply initial configuration to simulate Azure Stack Hub physical network stack. #> function Start-VirtualTor { [CmdletBinding()] param ( [Parameter()] [System.String] $VMName = 'TOR', [Parameter(Mandatory = $true)] [ValidateScript({Test-Path -Path $_ -Type Leaf})] [System.String] $ImagePath, [Parameter(Mandatory = $true)] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $true)] [System.Object[]] $Configuration, [Parameter()] [System.String[]] $DnsServer ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop Write-AzsReadinessLog -Message 'Starting virtual network environment' # Parsing configuration $mgmtSwitchName = 'Rack01-SwitchMgmt' $mgmtNetwork = $Configuration | Where-Object {$_.Name -eq $mgmtSwitchName} $mgmtHostIP = $mgmtNetwork.IPv4Gateway $bmcSwitchName = 'Rack01-BMCMgmt' $bmcNetwork = $Configuration | Where-Object {$_.Name -eq $bmcSwitchName} $bmcHostIp = ($bmcNetwork.Assignments | Where-Object {$_.Name -like '*DVM*'}).IPv4Address $externalSwitchName = 'CL01-External-VIPS' $externalNetwork = $Configuration | Where-Object {$_.Name -eq $externalSwitchName} $externalHostIp = $externalNetwork.IPv4FirstAddress if (-not ($mgmtHostIp -and $bmcHostIp -and $externalHostIp)) { Write-AzsReadinessLog 'Error parsing configuration' -Type 'Error' return } try { # Configure host virtual interfaces Assert-HostVNic -SwitchName $mgmtSwitchName -IPAddress $mgmtHostIP -PrefixLength $mgmtNetwork.IPv4MaskBits Assert-HostVNic -SwitchName $bmcSwitchName -IPAddress $bmcHostIP -PrefixLength $bmcNetwork.IPv4MaskBits -DefaultGateway $bmcNetwork.IPv4Gateway -DnsServer $DnsServer Assert-HostVNic -SwitchName $externalSwitchName -IPAddress $externalHostIP -PrefixLength $externalNetwork.IPv4MaskBits -DefaultGateway $externalNetwork.IPv4Gateway -DnsServer $DnsServer # Install and configure DHCP Server $scopes = @( @{ Name = $mgmtSwitchName ScopeId = $mgmtNetwork.IPv4NetworkAddress StartRange = $mgmtNetwork.IPv4FirstAddress EndRange = $mgmtNetwork.IPv4LastAddress SubnetMask = $mgmtNetwork.IPv4Mask } ) Install-DHCPServer -Scopes $scopes Write-AzsReadinessLog -Message "Configuring DHCP server binding" Get-DhcpServerv4Binding | Where-Object {$_.IPAddress -ne $mgmtHostIP} | Set-DhcpServerv4Binding -BindingState $false # Create SONiC virtual machine if (Get-VM | Where-Object {$_.Name -eq $VMName}) { Write-AzsReadinessLog -Message "Removing the existing virtual machine $vmName" Stop-VM -Name $VMName -TurnOff -Force Remove-VM -Name $VMName -Force } $imageParentPath = Split-Path -Path $ImagePath -Parent $imageFile = Get-Item -Path $ImagePath $diffFileName = $imageFile.BaseName + '.diff' + $imageFile.Extension $diffFilePath = Join-Path -Path $imageParentPath -ChildPath $diffFileName if (Test-Path -Path $diffFilePath) { Write-AzsReadinessLog -Message "Removing the image file $diffFilePath" Remove-Item -Path $diffFilePath -Force } Write-AzsReadinessLog -Message "Creating a new differencing image $diffFilePath" $null = New-VHD -Path $diffFilePath -ParentPath $ImagePath -Differencing Write-AzsReadinessLog -Message "Creating a new virtual machine $VMName" $null = New-VM -Name $VMName -MemoryStartupBytes 3GB -Generation 1 -VHDPath $diffFilePath -SwitchName $mgmtSwitchName Set-VM -Name $VMName -ProcessorCount 4 Write-AzsReadinessLog "Configuring virtual network adapters" Add-VMNetworkAdapter -VMName $VMName -Name 'Ethernet0' Add-VMNetworkAdapter -VMName $VMName -Name 'Ethernet4' Add-VMNetworkAdapter -VMName $VMName -Name 'Ethernet8' Get-VMNetworkAdapter -VMName $VMName -Name 'Ethernet*' | Set-VMNetworkAdapter -MacAddressSpoofing On Connect-VMNetworkAdapter -VMName $VMName -Name 'Ethernet4' -SwitchName $bmcSwitchName Connect-VMNetworkAdapter -VMName $VMName -Name 'Ethernet8' -SwitchName $externalSwitchName Write-AzsReadinessLog -Message "Starting the virtual machine $VMName" Start-VM -Name $VMName # Wait for the VM to obtain a management IP address from DHCP $vmMgmtIP = $null $ssh = $null $retry = 0 do { Write-AzsReadinessLog -Message "Waiting 10 seconds before attempting discovery" Start-Sleep -Seconds 10 $retry++ try { $dhcpIpAddresses = $null Write-AzsReadinessLog -Message "Getting DHCP assigned IP addresses" $dhcpIpAddresses = (Get-DhcpServerv4Lease -ScopeId $mgmtNetwork.IPv4NetworkAddress).IPAddress.IPAddressToString } catch { Write-AzsReadinessLog -Message "Failed getting DHCP assigned addresses. Error: $($_.Exception.Message)" } foreach ($dhcpIpAddress in $dhcpIpAddresses) { Write-AzsReadinessLog -Message "Testing DHCP-assigned IP address $dhcpIpAddress" if (Test-Connection -ComputerName $dhcpIpAddress -Count 1 -Quiet) { $vmMgmtIP = $dhcpIpAddress Write-AzsReadinessLog -Message "Ping to the virtual TOR at $vmMgmtIP successful" try { $ssh = New-SSHSession -ComputerName $vmMgmtIP -Credential $Credential -AcceptKey Write-AzsReadinessLog -Message "SSH connection to the virtual TOR $vmMgmtIP successful" } catch { Write-AzsReadinessLog -Message "Still waiting to establish SSH connection" } } } } until ($ssh -or $retry -ge 30) if (-not $ssh) { Write-AzsReadinessLog -Message "Unable to establish SSH connection with the virtual TOR after $retry attempts" -Type 'Error' return } } catch { Write-AzsReadinessLog -Message "Error occured: $($_.Exception.Message)" -Type 'Error' return } Write-AzsReadinessLog -Message "Sleeping for 30 seconds before returning to allow SONiC containers to start" Start-Sleep -Seconds 30 return $ssh } <# .SYNOPSIS Creates or validates the Hyper-V Management OS virtual network adapter with IP configuration. .DESCRIPTION For internal use. #> function Assert-HostVNic { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $SwitchName, [Parameter(Mandatory = $true)] [System.Net.IPAddress] $IPAddress, [Parameter(Mandatory = $true)] [System.Int32] $PrefixLength, [Parameter()] [System.Net.IPAddress] $DefaultGateway, [Parameter()] [System.String[]] $DnsServer ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop # Create the virtual switch if (Get-VMSwitch | Where-Object {$_.Name -eq $SwitchName}) { Write-AzsReadinessLog -Message "Using the existing Hyper-V switch $SwitchName" } else { Write-AzsReadinessLog -Message "Creating new Hyper-V Switch $SwitchName" $null = New-VMSwitch -Name $SwitchName -SwitchType Internal } # Configure internal host vNIC $vNic = Get-VMNetworkAdapter -ManagementOS -SwitchName $SwitchName $nic = Get-NetAdapter | Where-Object {$_.DeviceId -eq $vNic.DeviceId} foreach ($currentIP in (Get-NetIPAddress | Where-Object {$_.AddressFamily -eq 'IPv4' -and $_.InterfaceIndex -eq $nic.InterfaceIndex})) { if ($currentIP.IPAddress -eq $IPAddress -and $currentIP.PrefixLength -eq $PrefixLength) { Write-AzsReadinessLog -Message "IP address $IPAddress is already assigned to the host interface $($nic.InterfaceAlias)" $ipAssigned = $true } elseif ($currentIP.PrefixOrigin -eq 'Manual') { Write-AzsReadinessLog -Message "Removing IP address $($currentIP.IPAddress) from the host interface $($nic.InterfaceAlias)" Remove-NetIPAddress -InterfaceIndex $nic.InterfaceIndex -IPAddress $currentIP.IPAddress -Confirm:$false Start-Sleep -Seconds 5 } } if (-not $ipAssigned) { Write-AzsReadinessLog -Message "Setting IP Address $IPAddress on the host interface $($nic.InterfaceAlias)" if ($DefaultGateway) { $null = New-NetIPAddress -InterfaceIndex $nic.InterfaceIndex -IPAddress $IPAddress -PrefixLength $PrefixLength -DefaultGateway $DefaultGateway } else { $null = New-NetIPAddress -InterfaceIndex $nic.InterfaceIndex -IPAddress $IPAddress -PrefixLength $PrefixLength } Start-Sleep -Seconds 5 } if ($DnsServer) { Write-AzsReadinessLog -Message "Setting DNS Servers $DnsServer on the host interface $($nic.InterfaceAlias)" Set-DnsClientServerAddress -InterfaceIndex $nic.InterfaceIndex -ServerAddresses $DnsServer } } <# .SYNOPSIS Clean up Layer 3 test artefacts on the NRC host. .DESCRIPTION Remove the SONiC virtual machines and Hyper-V objects. #> function Stop-VirtualTor { [CmdletBinding()] param ( [Parameter()] [System.String] $VMName = 'TOR' ) $switchNames = @('Rack01-SwitchMgmt', 'Rack01-BMCMgmt', 'CL01-External-VIPS') Write-AzsReadinessLog -Message 'Stopping virtual network environment' try { if ($vm = Get-VM | Where-Object {$_.Name -eq $VMName}) { Write-AzsReadinessLog -Message "Removing the existing virtual machine $VMName" $vhdPath = $vm.HardDrives.Path Stop-VM -Name $VMName -TurnOff -Force Remove-VM -Name $VMName -Force Remove-Item -Path $vhdPath -Force } foreach ($vmSwitch in (Get-VMSwitch | Where-Object {$_.Name -in $switchNames -or $_.Name -like 'TOR-Uplink-*'})) { Write-AzsReadinessLog -Message "Removing Hyper-V virtual switch '$($vmSwitch.Name)'" Remove-VMSwitch -Name $vmSwitch.Name -Force } if (Get-Service | Where-Object {$_.Name -eq 'DhcpServer'}) { Write-AzsReadinessLog -Message "Removing DHCP Server" Remove-DHCPServer -Uninstall } } catch { Write-AzsReadinessLog -Message "Error occured: $($_.Exception.Message)" -Type 'Error' return $false } return $true } <# .SYNOPSIS Installs and configures DHCP server to be used for NRC management network. .DESCRIPTION Used in virtual TOR scenarios. .PARAMETER Scopes Array of hashtables of scope parameters. #> function Install-DHCPServer { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [System.Collections.Hashtable[]] $Scopes ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop try { if (-not (Get-Service | Where-Object {$_.Name -eq 'DhcpServer'})) { Write-AzsReadinessLog -Message 'Installing DHCP server and management tools.' $null = Install-WindowsFeature -Name DHCP -IncludeManagementTools } if ((Get-Service -Name 'DhcpServer').Status -ne [System.ServiceProcess.ServiceControllerStatus]::Running) { Write-AzsReadinessLog -Message 'Starting DHCP server service.' Start-Service -Name DhcpServer -WarningAction SilentlyContinue } if (-not (Get-Module -Name DhcpServer)) { Write-AzsReadinessLog -Message 'Importing DHCP server module.' Import-Module -Name DhcpServer } Write-AzsReadinessLog -Message 'Configuring DHCP server settings' Set-DhcpServerSetting -ConflictDetectionAttempts 1 New-ItemProperty -Path HKLM:\SYSTEM\CurrentControlSet\Services\DHCPServer\Parameters\ -Name 'DisableRogueDetection' -Value 1 -ErrorAction SilentlyContinue Restart-Service -Name DhcpServer -WarningAction SilentlyContinue foreach ($scope in $Scopes) { if (-not (Get-DhcpServerv4Scope | Where-Object {$_.ScopeId -eq $scope.ScopeId})) { Write-AzsReadinessLog -Message "Creating DHCP server scope '$($scope.Name)' for network '$($scope.ScopeId)'." $scopeParams = @{ Name = $scope.Name StartRange = $scope.StartRange EndRange = $scope.EndRange SubnetMask = $scope.SubnetMask State = 'Active' LeaseDuration = (New-TimeSpan -Days 10) } Add-DhcpServerv4Scope @scopeParams } } } catch { $errorMessage = "Failed installing DHCP server. Error: $($_.Exception.Message)" Write-AzsReadinessLog -Message $errorMessage throw $errorMessage } } <# .SYNOPSIS Removes DHCP server. .DESCRIPTION Used in virtual TOR scenarios. #> function Remove-DHCPServer { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] Param ( [Parameter()] [switch] $Uninstall ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop try { try { if (-not (Get-Module -Name DhcpServer)) { Write-AzsReadinessLog -Message 'Importing DHCP server module.' Import-Module -Name DhcpServer } } catch { Write-AzsReadinessLog -Message 'Failed importing DHCP server module.' } if (Get-Module -Name DhcpServer) { try { foreach ($scope in (Get-DhcpServerv4Scope)) { Write-AzsReadinessLog -Message "Removing scope id '$($scope.ScopeId)'." Remove-DhcpServerv4Scope -ScopeId $scope.ScopeId -Force } } catch { Write-AzsReadinessLog -Message "Failed removing DHCP server scopes. Error: $($_.Exception.Message)" } } if ( ($dhcpService = Get-Service | Where-Object {$_.Name -eq 'DhcpServer'}) -and ($dhcpService.Status -ne [System.ServiceProcess.ServiceControllerStatus]::Stopped) ) { Write-AzsReadinessLog -Message 'Stopping DHCP server service.' Stop-Service -Name 'DhcpServer' -WarningAction SilentlyContinue } if ($Uninstall) { $windowsFeatures = Get-WindowsFeature | Where-Object {$_.Installed} if ($windowsFeatures.Name -contains 'RSAT-DHCP') { Write-AzsReadinessLog -Message "Uninstalling DHCP management tools." $null = Uninstall-WindowsFeature -Name 'RSAT-DHCP' -WarningAction SilentlyContinue } if ($windowsFeatures.Name -contains 'DHCP') { Write-AzsReadinessLog -Message "Uninstalling DHCP server." $null = Uninstall-WindowsFeature -Name 'DHCP' -WarningAction SilentlyContinue } } } catch { $errorMessage = "Failed removing DHCP server. Error: $($_.Exception.Message)" Write-AzsReadinessLog -Message $errorMessage throw $errorMessage } } <# .SYNOPSIS Applies configuration to the virtual TOR. .DESCRIPTION Used in virtual TOR scenarios. #> function Set-TorConfig { [CmdletBinding()] Param ( [Parameter()] [System.String] $ComputerName, [Parameter(Mandatory = $true)] [System.Management.Automation.PSCredential] $Credential, [Parameter()] [SSH.SshSession] $SSHSession, [Parameter()] [switch] $Clear, [Parameter()] [System.Int32] $LocalAsn, [Parameter()] [System.Net.IPAddress] $LoopbackAddress, [Parameter()] [System.String] $InterfaceName = 'Ethernet0', [Parameter()] [System.Net.IPAddress] $LocalAddress, [Parameter()] [System.Int32] $PrefixLength = 30, [Parameter()] [System.Net.IPAddress] $PeerAddress, [Parameter()] [System.Int32] $RemoteAsn, [Parameter()] [System.String] $BmcNetworkInterfaceName = 'Ethernet4', [Parameter()] [System.Net.IPAddress] $BmcNetworkAddress, [Parameter()] [System.Int32] $BmcNetworkPrefixLength, [Parameter()] [System.Int32] $BmcNetworkVlanId = 125, [Parameter()] [System.String] $ExternalNetworkInterfaceName = 'Ethernet8', [Parameter()] [System.Net.IPAddress] $ExternalNetworkAddress, [Parameter()] [System.Int32] $ExternalNetworkPrefixLength, [Parameter()] [System.Int32] $ExternalNetworkVlanId = 1000 ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop try { $workingDirectory = Join-Path -Path $env:TEMP -ChildPath (New-Guid) Write-AzsReadinessLog -Message "Using temporary folder $workingDirectory" $null = New-Item -Path $workingDirectory -Itemtype Directory if (-not $ComputerName) { $ComputerName = $SSHSession.Host } if ($SSHSession.Host -eq $Computername -and $SSHSession.Connected) { Write-AzsReadinessLog -Message "Using existing SSH session" $ComputerName = $SSHSession.Host } else { Write-AzsReadinessLog -Message "Establishing new SSH session with $ComputerName" $SSHSession = New-SSHSession -ComputerName $ComputerName -Credential $Credential -AcceptKey } Write-AzsReadinessLog -Message "Retrieving current configuration file" $localFilePath = Join-Path -Path $workingDirectory -ChildPath 'config_db.json' Get-SCPFile -ComputerName $ComputerName -Credential $Credential -AcceptKey -RemoteFile '/etc/sonic/config_db.json' -LocalFile $localFilePath $config = Get-Content -Path $localFilePath -Raw | ConvertFrom-Json if ($Clear) { Write-AzsReadinessLog -Message "Clearing configuration" $newConfig = [PSCustomObject]@{ DEVICE_METADATA = $config.DEVICE_METADATA PORT = $config.PORT LOOPBACK_INTERFACE = @{} INTERFACE = @{} BGP_NEIGHBOR = @{} VLAN = @{} VLAN_MEMBER = @{} VLAN_INTERFACE= @{} } } else { $newConfig = $config } # Generate a new random MAC address for the device $arrMac = $newConfig.DEVICE_METADATA.localhost.mac -split ':' for ($i = 3; $i -le 5; $i++) { $arrMac[$i] = [System.String]::Format('{0:X2}', (Get-Random -Maximum 255)) } $newConfig.DEVICE_METADATA.localhost.mac = $arrMac -join ':' if ($LocalAsn) { Write-AzsReadinessLog -Message "Local ASN = $LocalAsn" $newConfig.DEVICE_METADATA.localhost.bgp_asn = $LocalAsn } if ($LoopbackAddress) { Write-AzsReadinessLog -Message "Loopback address = $LoopbackAddress" $newConfig.LOOPBACK_INTERFACE = [PSCustomObject]@{ "Loopback0|$LoopbackAddress/32" = @{} } } if ($LocalAddress) { Write-AzsReadinessLog -Message "Interface $InterfaceName address = $LocalAddress/$PrefixLength" $newConfig.INTERFACE = [PSCustomObject]@{ "$InterfaceName|$LocalAddress/$PrefixLength" = @{} } } if ($PeerAddress) { Write-AzsReadinessLog -Message "BGP Neighbor address = $PeerAddress with ASN = $RemoteAsn" $newConfig.BGP_NEIGHBOR = [PSCustomObject]@{ "$PeerAddress" = @{ local_addr = $LocalAddress asn = $RemoteAsn name = "Uplink" } } } if ($BmcNetworkAddress -and $ExternalNetworkAddress) { $newConfig.VLAN = [PSCustomObject]@{ "Vlan$BmcNetworkVlanId" = @{ members = @($BmcNetworkInterfaceName) vlanid = $BmcNetworkVlanId } "Vlan$ExternalNetworkVlanId" = @{ members = @($ExternalNetworkInterfaceName) vlanid = $ExternalNetworkVlanId } } $newConfig.VLAN_MEMBER = [PSCustomObject]@{ "Vlan$BmcNetworkVlanId|$BmcNetworkInterfaceName" = @{ tagging_mode = "untagged" } "Vlan$ExternalNetworkVlanId|$ExternalNetworkInterfaceName" = @{ tagging_mode = "untagged" } } $newConfig.VLAN_INTERFACE = [PSCustomObject]@{ "Vlan$BmcNetworkVlanId|$BmcNetworkAddress/$BmcNetworkPrefixLength" = @{} "Vlan$ExternalNetworkVlanId|$ExternalNetworkAddress/$ExternalNetworkPrefixLength" = @{} } } Write-AzsReadinessLog -Message "Saving updated configuration file" Set-Content -Value ($newConfig | ConvertTo-Json).Replace("`r`n","`n") -Path $localFilePath -NoNewline Set-SCPFile -ComputerName $ComputerName -Credential $Credential -AcceptKey $true -LocalFile $localFilePath -RemotePath '/home/admin' $sshOutput = Invoke-SSHCommand -Command 'sudo cp /home/admin/config_db.json /etc/sonic' -SSHSession $SSHSession -EnsureConnection $sshOutput = Invoke-SSHCommand -Command 'sudo config reload -y' -SSHSession $SSHSession -Timeout 120 -EnsureConnection foreach ($line in $sshOutput.Output) { Write-AzsReadinessLog -Message $line } } catch { Write-AzsReadinessLog -Message "Error occured: $($_.Exception.Message)" -Type 'Error' throw $_ } finally { if (Test-Path -Path $workingDirectory) { Write-AzsReadinessLog -Message "Removing temporary folder $workingDirectory" Remove-Item -Path $workingDirectory -Recurse -Force } } } <# .SYNOPSIS Runs a command on the virtual TOR and returns formatted output. .DESCRIPTION Used in virtual TOR scenarios. #> function Invoke-TorCommand { [CmdletBinding()] Param ( [Parameter(Mandatory = $true)] [SSH.SshSession] $SSHSession, [Parameter(Mandatory = $true)] [System.Management.Automation.PSCredential] $Credential, [Parameter(Mandatory = $true)] [System.String] $Command, [Parameter()] [System.Int32] $Timeout = 10 ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop try { if ($SSHSession.Connected) { Write-AzsReadinessLog -Message "Using existing SSH session" } else { $computerName = $SSHSession.Host Write-AzsReadinessLog -Message "Establishing new SSH session with $computerName" $SSHSession = New-SSHSession -ComputerName $computerName -Credential $Credential -AcceptKey } $retry = 0 do { Write-AzsReadinessLog -Message "Invoking command '$Command' on TOR" $sshOutput = Invoke-SSHCommand -Command $Command -SSHSession $SSHSession -Timeout $Timeout -EnsureConnection Write-AzsReadinessLog -Message "Command returned status code $($sshOutput.ExitStatus)" if ($sshOutput.ExitStatus -ne 0) { $retry++ Write-AzsReadinessLog -Message "Sleeping for 5 seconds" Start-Sleep -Seconds 5 } } until (($sshOutput.ExitStatus -eq 0) -or ($retry -ge 10)) foreach ($line in $sshOutput.Output) { Write-AzsReadinessLog -Message $line } if ($sshOutput.ExitStatus -ne 0) { throw "Error running SSH command, status code = $($sshOutput.ExitStatus)" } return $sshOutput.Output } catch { Write-AzsReadinessLog -Message "Error occured: $($_.Exception.Message)" -Type 'Error' throw $_ } } <# .SYNOPSIS Configures the external connection on the virtual TOR. .DESCRIPTION Used in virtual TOR scenarios. #> function Connect-VirtualTorUplink { [CmdletBinding()] Param ( [Parameter()] [System.String] $VMName = 'TOR', [Parameter(Mandatory = $true)] [System.String] $NetAdapterInterfaceDescription, [Parameter()] [System.String] $VNicName = 'Ethernet0' ) $ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop try { if (-not ($netAdapter = Get-NetAdapter | Where-Object {$_.InterfaceDescription -eq $NetAdapterInterfaceDescription})) { throw "Network adapter with description $NetAdapterInterfaceDescription not found" } if ($vmSwitch = Get-VMSwitch | Where-Object {$_.NetAdapterInterfaceDescription -eq $NetAdapterInterfaceDescription}) { Write-AzsReadinessLog "Using existing Hyper-V switch $($vmSwitch.Name)" } else { $vmSwitchName = "TOR-Uplink-$($netAdapter.InterfaceIndex)" $retry = 1 do { try { Write-AzsReadinessLog "Creating new Hyper-V external switch $vmSwitchName, attempt $retry" $vmSwitch = New-VMSwitch -Name $vmSwitchName -NetAdapterInterfaceDescription $NetAdapterInterfaceDescription -AllowManagementOS $false $retry = 10 } catch { Start-Sleep -Seconds 1 $retry++ if ($retry -eq 10) { throw $_ } } } until ($retry -eq 10) } Write-AzsReadinessLog "Connecting virtual machine $VMName interface $VNicName to the external Hyper-V switch $($vmSwitch.Name)" Connect-VMNetworkAdapter -VMName $VMName -Name $VNicName -SwitchName $vmSwitch.Name } catch { Write-AzsReadinessLog -Message "Error occured: $($_.Exception.Message)" -Type 'Error' throw $_ } } # SIG # Begin signature block # MIIjmwYJKoZIhvcNAQcCoIIjjDCCI4gCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCA4kW5HwFVAk0Cb # 2j15/v1p2CDQzcCGrrTzBqWXSvt6cKCCDYEwggX/MIID56ADAgECAhMzAAAB32vw # LpKnSrTQAAAAAAHfMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjAxMjE1MjEzMTQ1WhcNMjExMjAyMjEzMTQ1WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQC2uxlZEACjqfHkuFyoCwfL25ofI9DZWKt4wEj3JBQ48GPt1UsDv834CcoUUPMn # s/6CtPoaQ4Thy/kbOOg/zJAnrJeiMQqRe2Lsdb/NSI2gXXX9lad1/yPUDOXo4GNw # PjXq1JZi+HZV91bUr6ZjzePj1g+bepsqd/HC1XScj0fT3aAxLRykJSzExEBmU9eS # yuOwUuq+CriudQtWGMdJU650v/KmzfM46Y6lo/MCnnpvz3zEL7PMdUdwqj/nYhGG # 3UVILxX7tAdMbz7LN+6WOIpT1A41rwaoOVnv+8Ua94HwhjZmu1S73yeV7RZZNxoh # EegJi9YYssXa7UZUUkCCA+KnAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUOPbML8IdkNGtCfMmVPtvI6VZ8+Mw # UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1 # ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDYzMDA5MB8GA1UdIwQYMBaAFEhu # ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu # bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w # Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3 # Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx # MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAnnqH # tDyYUFaVAkvAK0eqq6nhoL95SZQu3RnpZ7tdQ89QR3++7A+4hrr7V4xxmkB5BObS # 0YK+MALE02atjwWgPdpYQ68WdLGroJZHkbZdgERG+7tETFl3aKF4KpoSaGOskZXp # TPnCaMo2PXoAMVMGpsQEQswimZq3IQ3nRQfBlJ0PoMMcN/+Pks8ZTL1BoPYsJpok # t6cql59q6CypZYIwgyJ892HpttybHKg1ZtQLUlSXccRMlugPgEcNZJagPEgPYni4 # b11snjRAgf0dyQ0zI9aLXqTxWUU5pCIFiPT0b2wsxzRqCtyGqpkGM8P9GazO8eao # mVItCYBcJSByBx/pS0cSYwBBHAZxJODUqxSXoSGDvmTfqUJXntnWkL4okok1FiCD # Z4jpyXOQunb6egIXvkgQ7jb2uO26Ow0m8RwleDvhOMrnHsupiOPbozKroSa6paFt # VSh89abUSooR8QdZciemmoFhcWkEwFg4spzvYNP4nIs193261WyTaRMZoceGun7G # CT2Rl653uUj+F+g94c63AhzSq4khdL4HlFIP2ePv29smfUnHtGq6yYFDLnT0q/Y+ # Di3jwloF8EWkkHRtSuXlFUbTmwr/lDDgbpZiKhLS7CBTDj32I0L5i532+uHczw82 # oZDmYmYmIUSMbZOgS65h797rj5JJ6OkeEUJoAVwwggd6MIIFYqADAgECAgphDpDS # AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK # V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0 # IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0 # ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla # MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS # ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT # H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB # AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG # OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S # 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz # y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7 # 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u # M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33 # X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl # XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP # 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB # l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF # RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM # CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ # BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud # DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO # 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0 # LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y # Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p # Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y # Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB # FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw # cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA # XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY # 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj # 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd # d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ # Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf # wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ # aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j # NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B # xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96 # eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7 # r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I # RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIVcDCCFWwCAQEwgZUwfjELMAkG # A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx # HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z # b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAd9r8C6Sp0q00AAAAAAB3zAN # BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor # BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgiP0Yc5LR # +MqmO7f2F5H9+JwS3mxj7pQi7loUnSYHWLQwQgYKKwYBBAGCNwIBDDE0MDKgFIAS # AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN # BgkqhkiG9w0BAQEFAASCAQBFUXEhQR7uXCvettPdsGW4muAsLrxpgEjjhjKmqhwp # zFtIyx0aeOjC+a1VNOkRitKm17u+Q9j1h0XoA9uqbz1DPN3avOhNQIY8zQ6mxMD4 # PBpF1HLgRGQF24k+g7JkO1HBtTO40y8fZs2J1YZ9ocwzQlHj0Vua2Ez6kMoXqfSg # C5l3CTsYUztfT7By+Bkae9gXd2FAPbr5UzV1U9evoVvDlVdAXG4JzBVP1r2Pn+Uu # O0K54kWVZWbbye03Ju51OV6W4L4P6T0423Fip7iBa5R0IjUJyFXZq4M/TQaAxibV # pjMpfAD1qlao+Arlxe45PBnfb1Oe4kadWLBAxNMqXCXLoYIS+jCCEvYGCisGAQQB # gjcDAwExghLmMIIS4gYJKoZIhvcNAQcCoIIS0zCCEs8CAQMxDzANBglghkgBZQME # AgEFADCCAVgGCyqGSIb3DQEJEAEEoIIBRwSCAUMwggE/AgEBBgorBgEEAYRZCgMB # MDEwDQYJYIZIAWUDBAIBBQAEILCZgdX+W7M8I186Ywe01+wDbr+Pbf122ESvhpTv # MZe/AgZgY06vepkYEjIwMjEwNDE5MTU1MTM2LjU3WjAEgAIB9KCB2KSB1TCB0jEL # MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v # bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWlj # cm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFs # ZXMgVFNTIEVTTjo4NkRGLTRCQkMtOTMzNTElMCMGA1UEAxMcTWljcm9zb2Z0IFRp # bWUtU3RhbXAgU2VydmljZaCCDkowggT5MIID4aADAgECAhMzAAABPs7Kd1LF9zQr # AAAAAAE+MA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX # YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg # Q29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAy # MDEwMB4XDTIwMTAxNTE3MjgyNVoXDTIyMDExMjE3MjgyNVowgdIxCzAJBgNVBAYT # AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTArBgNVBAsTJE1pY3Jvc29mdCBJ # cmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEmMCQGA1UECxMdVGhhbGVzIFRTUyBF # U046ODZERi00QkJDLTkzMzUxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1w # IFNlcnZpY2UwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC8VMTIPNl+ # nCzjTiBILSS3hVLJf+9rHA5+uLz2BB3G99A2+9ABF5spHemWofPRkdlb5uYXHIa1 # OH3PDbQtJ2kxxZgMVzWvM+4m9M0CcOQrJA/5OqtbuP+UOUItuqLy5ujgSpKmQetr # Rm3XmPav8gkZlu7dBpFjqpgxnHGSTDhjm5sDBXcTWn5M3MWDyfOAn2TAQzjG9kB/ # 02EeEzYr+PHT3bGYrHIV+nRfS1uhj13U7KF0JeXyyk6KATfaDzMfXZjY1dN8jjXj # UtBT710o4pDtgUXWTCh+4YbDExTQKwOKY4NaCvpUVVw0N3a1Bsa5uB18sEYQF+N7 # Q/Kg45cQ7WbhAgMBAAGjggEbMIIBFzAdBgNVHQ4EFgQUk1rznfi70GIta/C1tlQO # toaI/XswHwYDVR0jBBgwFoAU1WM6XIoxkPNDe3xGG8UzaFqFbVUwVgYDVR0fBE8w # TTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVj # dHMvTWljVGltU3RhUENBXzIwMTAtMDctMDEuY3JsMFoGCCsGAQUFBwEBBE4wTDBK # BggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9N # aWNUaW1TdGFQQ0FfMjAxMC0wNy0wMS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUE # DDAKBggrBgEFBQcDCDANBgkqhkiG9w0BAQsFAAOCAQEAprP5EX1an4aSuRWPpxjl # 2MJ1V6kkXK58AEnWoqUJZeE6hgBwHvDtnHNELhnaJjhtz1BT3exrZgPCFDAU96p8 # pl9ZKSaty6zj1AH0QY9z0XAiB8FArYAm2FpgTKxNrBLjR/rJzrD/Jui0ByWoUCv4 # E8O3TMZmgTG8ZzxmlUBmm9LJdvMYu4q2bwr5HvdULgNSnixEVyTULHwgu9h1hI1i # o5HKHQbCLe/gdabDoe61p8U50WNopARxKyfRI0t9jbmo6qe7oMv40CjvPeoPR4EM # hKKVahvl2WUNw41+y731QS06ett2Xb3bIY0jLGKWkjxcY2AZxnEo3pWosHEC4qVY # 5jCCBnEwggRZoAMCAQICCmEJgSoAAAAAAAIwDQYJKoZIhvcNAQELBQAwgYgxCzAJ # BgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25k # MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jv # c29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDEwMB4XDTEwMDcwMTIx # MzY1NVoXDTI1MDcwMTIxNDY1NVowfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh # c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD # b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw # MTAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpHQ28dxGKOiDs/BOX # 9fp/aZRrdFQQ1aUKAIKF++18aEssX8XD5WHCdrc+Zitb8BVTJwQxH0EbGpUdzgkT # jnxhMFmxMEQP8WCIhFRDDNdNuDgIs0Ldk6zWczBXJoKjRQ3Q6vVHgc2/JGAyWGBG # 8lhHhjKEHnRhZ5FfgVSxz5NMksHEpl3RYRNuKMYa+YaAu99h/EbBJx0kZxJyGiGK # r0tkiVBisV39dx898Fd1rL2KQk1AUdEPnAY+Z3/1ZsADlkR+79BL/W7lmsqxqPJ6 # Kgox8NpOBpG2iAg16HgcsOmZzTznL0S6p/TcZL2kAcEgCZN4zfy8wMlEXV4WnAEF # TyJNAgMBAAGjggHmMIIB4jAQBgkrBgEEAYI3FQEEAwIBADAdBgNVHQ4EFgQU1WM6 # XIoxkPNDe3xGG8UzaFqFbVUwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYD # VR0PBAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxi # aNE9lJBb186aGMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3Nv # ZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMu # Y3JsMFoGCCsGAQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNy # b3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQw # gaAGA1UdIAEB/wSBlTCBkjCBjwYJKwYBBAGCNy4DMIGBMD0GCCsGAQUFBwIBFjFo # dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vUEtJL2RvY3MvQ1BTL2RlZmF1bHQuaHRt # MEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAFAAbwBsAGkAYwB5AF8AUwB0 # AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQAH5ohRDeLG4Jg/ # gXEDPZ2joSFvs+umzPUxvs8F4qn++ldtGTCzwsVmyWrf9efweL3HqJ4l4/m87WtU # VwgrUYJEEvu5U4zM9GASinbMQEBBm9xcF/9c+V4XNZgkVkt070IQyK+/f8Z/8jd9 # Wj8c8pl5SpFSAK84Dxf1L3mBZdmptWvkx872ynoAb0swRCQiPM/tA6WWj1kpvLb9 # BOFwnzJKJ/1Vry/+tuWOM7tiX5rbV0Dp8c6ZZpCM/2pif93FSguRJuI57BlKcWOd # eyFtw5yjojz6f32WapB4pm3S4Zz5Hfw42JT0xqUKloakvZ4argRCg7i1gJsiOCC1 # JeVk7Pf0v35jWSUPei45V3aicaoGig+JFrphpxHLmtgOR5qAxdDNp9DvfYPw4Ttx # Cd9ddJgiCGHasFAeb73x4QDf5zEHpJM692VHeOj4qEir995yfmFrb3epgcunCaw5 # u+zGy9iCtHLNHfS4hQEegPsbiSpUObJb2sgNVZl6h3M7COaYLeqN4DMuEin1wC9U # JyH3yKxO2ii4sanblrKnQqLJzxlBTeCG+SqaoxFmMNO7dDJL32N79ZmKLxvHIa9Z # ta7cRDyXUHHXodLFVeNp3lfB0d4wwP3M5k37Db9dT+mdHhk4L7zPWAUu7w2gUDXa # 7wknHNWzfjUeCLraNtvTX4/edIhJEqGCAtQwggI9AgEBMIIBAKGB2KSB1TCB0jEL # MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v # bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWlj # cm9zb2Z0IElyZWxhbmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFs # ZXMgVFNTIEVTTjo4NkRGLTRCQkMtOTMzNTElMCMGA1UEAxMcTWljcm9zb2Z0IFRp # bWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIaAxUAoEwV6PTGMJOMKTWxN1Mp # r5PMkNSggYMwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv # bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 # aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkq # hkiG9w0BAQUFAAIFAOQngbQwIhgPMjAyMTA0MTkxMjEyMzZaGA8yMDIxMDQyMDEy # MTIzNlowdDA6BgorBgEEAYRZCgQBMSwwKjAKAgUA5CeBtAIBADAHAgEAAgIL1TAH # AgEAAgISEzAKAgUA5CjTNAIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZ # CgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAMtY # MVxLoShZizrWI1YxGCxs6H9vx9oI+hLqeLk7G2YtAulCpvuG4J79Be4pSvhO51Qp # ls9N2rCKylg/SrZ90y3MbcSmUPLdJKTOMQ8paR3Izsf3mvJ1UvKrI7MF8cnyUIbJ # RP1gwLxBvUVlRiBOgTKXLIIflbznUiKOCgDW9y7oMYIDDTCCAwkCAQEwgZMwfDEL # MAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1v # bmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWlj # cm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAE+zsp3UsX3NCsAAAAAAT4w # DQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAv # BgkqhkiG9w0BCQQxIgQg8tkw6JaP4zAvri0i/4P948el3lYgpu+YU1AtUT0SFm0w # gfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCCL686Nqo1O8o5ka63j0deuq3BS # PZkKdU66sHB+BDGbEzCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX # YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg # Q29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAy # MDEwAhMzAAABPs7Kd1LF9zQrAAAAAAE+MCIEIMmQLrPNd3yfJYCF7IF6mfpORy2v # Ri8j7XuJomqNjy4nMA0GCSqGSIb3DQEBCwUABIIBAGH1PhkzTwjBh9rzBGyYiRCN # Rw9ssH6bl+Qo5oEVVp6IvPABACMvcINA3gqtr6Gc0VdUg2pIeByhYtn76RstYD+j # dzZ1g4TLe65ck9+XGBbF2WhV0DiYiK10ltwv5mgUG+LMonjHHaihTGUAR6dM5/MA # vQ8f29XKbSuyxTQWWSmaqGQy8HnndHcp9KUi5xmgJoEqjbNUdXh5n2SpnZWjuUdN # HnaNH3ydvkTk+vhVFyXK9bv56ums6CVLPdjM9ANoa+KbuPyIug59lDohS9NtwOD9 # sxutDRyjIVSfVX3/RSWU8IFA8N+NXOqujTAjc2BNAuZBHpOQC8bPRFQghV7rCWg= # SIG # End signature block |