|
| 1 | +# Parameter sets in Powershell are a bit counter-intuitive. See here (https://docs.microsoft.com/en-us/powershell/scripting/developer/cmdlet/cmdlet-parameter-sets?view=powershell-7) for details |
| 2 | +param( |
| 3 | + [Parameter(Mandatory = $true, HelpMessage = "Name of the test (proposed) subscription")] |
| 4 | + [string]$Subscription, |
| 5 | + [Parameter(ParameterSetName="BenchmarkSubscription", Mandatory = $true, HelpMessage = "Name of the benchmark subscription to compare against")] |
| 6 | + [string]$BenchmarkSubscription, |
| 7 | + [Parameter(ParameterSetName="BenchmarkConfig", Mandatory = $true, HelpMessage = "Path to the benchmark config to compare against")] |
| 8 | + [string]$BenchmarkConfig, |
| 9 | + [Parameter(Mandatory = $false, HelpMessage = "Print verbose logging messages")] |
| 10 | + [switch]$VerboseLogging = $false |
| 11 | +) |
| 12 | + |
| 13 | +# Install required modules |
| 14 | +if (-Not $(Get-Module -ListAvailable -Name Az)) { Install-Package Az -Force} |
| 15 | +if (-Not $(Get-Module -ListAvailable -Name Communary.PASM)) { Install-Package Communary.PASM -Force} |
| 16 | + |
| 17 | +# Import modules |
| 18 | +Import-Module Az |
| 19 | +Import-Module Communary.PASM |
| 20 | +Import-Module $PSScriptRoot/../common_powershell/Logging.psm1 -Force |
| 21 | + |
| 22 | +function Select-ClosestMatch { |
| 23 | + param ( |
| 24 | + [Parameter(Position = 0)][ValidateNotNullOrEmpty()] |
| 25 | + [string] $Value, |
| 26 | + [Parameter(Position = 1)][ValidateNotNullOrEmpty()] |
| 27 | + [System.Array] $Array |
| 28 | + ) |
| 29 | + $Array | Sort-Object @{Expression={ Get-PasmScore -String1 $Value -String2 $_ -Algorithm "LevenshteinDistance" }; Ascending=$false} | Select-Object -First 1 |
| 30 | +} |
| 31 | + |
| 32 | +# Compare two NSG rule sets |
| 33 | +# Match parameter-by-parameter |
| 34 | +# -------------------------------------------- |
| 35 | +function Compare-NSGRules { |
| 36 | + param ( |
| 37 | + [Parameter()] |
| 38 | + [System.Array] $BenchmarkRules, |
| 39 | + [Parameter()] |
| 40 | + [System.Array] $TestRules |
| 41 | + ) |
| 42 | + $nMatched = 0 |
| 43 | + $unmatched = @() |
| 44 | + foreach ($benchmarkRule in $BenchmarkRules) { |
| 45 | + $lowestDifference = [double]::PositiveInfinity |
| 46 | + $closestMatchingRule = $null |
| 47 | + # Iterate over TestRules checking for an identical match by checking how many of the rule parameters differ |
| 48 | + # If an exact match is found then increment the counter, otherwise log the rule and the closest match |
| 49 | + foreach ($testRule in $TestRules) { |
| 50 | + $difference = 0 |
| 51 | + if ($benchmarkRule.Protocol -ne $testRule.Protocol) { $difference += 1 } |
| 52 | + if ([string]($benchmarkRule.SourcePortRange) -ne [string]($testRule.SourcePortRange)) { $difference += 1 } |
| 53 | + if ([string]($benchmarkRule.DestinationPortRange) -ne [string]($testRule.DestinationPortRange)) { $difference += 1 } |
| 54 | + if ([string]($benchmarkRule.SourceAddressPrefix) -ne [string]($testRule.SourceAddressPrefix)) { $difference += 1 } |
| 55 | + if ([string]($benchmarkRule.DestinationAddressPrefix) -ne [string]($testRule.DestinationAddressPrefix)) { $difference += 1 } |
| 56 | + if ($benchmarkRule.Access -ne $testRule.Access) { $difference += 1 } |
| 57 | + if ($benchmarkRule.Priority -ne $testRule.Priority) { $difference += 1 } |
| 58 | + if ($benchmarkRule.Direction -ne $testRule.Direction) { $difference += 1 } |
| 59 | + if ($difference -lt $lowestDifference) { |
| 60 | + $lowestDifference = $difference |
| 61 | + $closestMatchingRule = $testRule |
| 62 | + } |
| 63 | + if ($difference -eq 0) { break } |
| 64 | + } |
| 65 | + |
| 66 | + if ($lowestDifference -eq 0) { |
| 67 | + $nMatched += 1 |
| 68 | + if ($VerboseLogging) { Add-LogMessage -Level Info "Found matching rule for $($benchmarkRule.Name)" } |
| 69 | + } else { |
| 70 | + Add-LogMessage -Level Error "Could not find an identical rule for $($benchmarkRule.Name)" |
| 71 | + $unmatched += $benchmarkRule.Name |
| 72 | + $benchmarkRule | Out-String |
| 73 | + Add-LogMessage -Level Info "Closest match was:" |
| 74 | + $closestMatchingRule | Out-String |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + $nTotal = $nMatched + $unmatched.Count |
| 79 | + if ($nMatched -eq $nTotal) { |
| 80 | + Add-LogMessage -Level Success "Matched $nMatched/$nTotal rules" |
| 81 | + } else { |
| 82 | + Add-LogMessage -Level Failure "Matched $nMatched/$nTotal rules" |
| 83 | + } |
| 84 | +} |
| 85 | + |
| 86 | + |
| 87 | +function Test-OutboundConnection { |
| 88 | + param ( |
| 89 | + [Parameter(Position = 0)][ValidateNotNullOrEmpty()] |
| 90 | + [Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine] $VM, |
| 91 | + [Parameter(Position = 0)][ValidateNotNullOrEmpty()] |
| 92 | + [string] $DestinationAddress, |
| 93 | + [Parameter(Position = 1)][ValidateNotNullOrEmpty()] |
| 94 | + [string] $DestinationPort |
| 95 | + ) |
| 96 | + # Get the network watcher, creating a new one if required |
| 97 | + $networkWatcher = Get-AzNetworkWatcher | Where-Object { $_.Location -eq $VM.Location } |
| 98 | + if (-Not $networkWatcher) { |
| 99 | + $networkWatcher = New-AzNetworkWatcher -Name "NetworkWatcher" -ResourceGroupName "NetworkWatcherRG" -Location $VM.Location |
| 100 | + } |
| 101 | + # Ensure that the VM has the extension installed (if we have permissions for this) |
| 102 | + $networkWatcherExtension = Get-AzVMExtension -ResourceGroupName $VM.ResourceGroupName -VMName $VM.Name | Where-Object { ($_.Publisher -eq "Microsoft.Azure.NetworkWatcher") -and ($_.ProvisioningState -eq "Succeeded") } |
| 103 | + if (-Not $networkWatcherExtension) { |
| 104 | + Add-LogMessage -Level Info "... registering the Azure NetworkWatcher extension on $($VM.Name). " |
| 105 | + # Add the Windows extension |
| 106 | + if ($VM.OSProfile.WindowsConfiguration) { |
| 107 | + $_ = Set-AzVMExtension -ResourceGroupName $VM.ResourceGroupName -VMName $VM.Name -Location $VM.Location -Name "AzureNetworkWatcherExtension" -Publisher "Microsoft.Azure.NetworkWatcher" -Type "NetworkWatcherAgentWindows" -TypeHandlerVersion "1.4" -ErrorVariable NotInstalled -ErrorAction SilentlyContinue |
| 108 | + if ($NotInstalled) { |
| 109 | + Add-LogMessage -Level Warning "Unable to register Windows network watcher extension for $($VM.Name)" |
| 110 | + return "Unknown" |
| 111 | + } |
| 112 | + } |
| 113 | + # Add the Linux extension |
| 114 | + if ($VM.OSProfile.LinuxConfiguration) { |
| 115 | + $_ = Set-AzVMExtension -ResourceGroupName $VM.ResourceGroupName -VMName $VM.Name -Location $VM.Location -Name "AzureNetworkWatcherExtension" -Publisher "Microsoft.Azure.NetworkWatcher" -Type "NetworkWatcherAgentLinux" -TypeHandlerVersion "1.4" -ErrorVariable NotInstalled -ErrorAction SilentlyContinue |
| 116 | + if ($NotInstalled) { |
| 117 | + Add-LogMessage -Level Warning "Unable to register Linux network watcher extension for $($VM.Name)" |
| 118 | + return "Unknown" |
| 119 | + } |
| 120 | + } |
| 121 | + } |
| 122 | + Add-LogMessage -Level Info "... testing connectivity on port $DestinationPort" |
| 123 | + $networkCheck = Test-AzNetworkWatcherConnectivity -NetworkWatcher $networkWatcher -SourceId $VM.Id -DestinationAddress $DestinationAddress -DestinationPort $DestinationPort -ErrorVariable NotAvailable -ErrorAction SilentlyContinue |
| 124 | + if ($NotAvailable) { |
| 125 | + Add-LogMessage -Level Warning "Unable to test connection for $($VM.Name)" |
| 126 | + return "Unknown" |
| 127 | + } else { |
| 128 | + return $networkCheck.ConnectionStatus |
| 129 | + } |
| 130 | +} |
| 131 | + |
| 132 | +function Convert-RuleToEffectiveRule { |
| 133 | + param ( |
| 134 | + [Parameter(Position = 0)][ValidateNotNullOrEmpty()] |
| 135 | + [System.Object] $rule |
| 136 | + ) |
| 137 | + $effectiveRule = [Microsoft.Azure.Commands.Network.Models.PSEffectiveSecurityRule]::new() |
| 138 | + $effectiveRule.Name = $rule.Name |
| 139 | + $effectiveRule.Protocol = $rule.Protocol.Replace("*", "All") |
| 140 | + # Source port range |
| 141 | + $effectiveRule.SourcePortRange = New-Object System.Collections.Generic.List[string] |
| 142 | + foreach ($port in $rule.SourcePortRange) { |
| 143 | + # We do not explicitly deal with the case where the port is not an integer, a range or '*' |
| 144 | + if ($port -eq "*") { $effectiveRule.SourcePortRange.Add("0-65535"); break } |
| 145 | + elseif ($port.Contains("-")) { $effectiveRule.SourcePortRange.Add($port) } |
| 146 | + else { $effectiveRule.SourcePortRange.Add("$port-$port") } |
| 147 | + } |
| 148 | + # Destination port range |
| 149 | + $effectiveRule.DestinationPortRange = New-Object System.Collections.Generic.List[string] |
| 150 | + foreach ($port in $rule.DestinationPortRange) { |
| 151 | + # We do not explicitly deal with the case where the port is not an integer, a range or '*' |
| 152 | + if ($port -eq "*") { $effectiveRule.DestinationPortRange.Add("0-65535"); break } |
| 153 | + elseif ($port.Contains("-")) { $effectiveRule.DestinationPortRange.Add($port) } |
| 154 | + else { $effectiveRule.DestinationPortRange.Add("$port-$port") } |
| 155 | + } |
| 156 | + # Source address prefix |
| 157 | + $effectiveRule.SourceAddressPrefix = New-Object System.Collections.Generic.List[string] |
| 158 | + foreach ($prefix in $rule.SourceAddressPrefix) { |
| 159 | + if ($prefix -eq "0.0.0.0/0") { $effectiveRule.SourceAddressPrefix.Add("*"); break } |
| 160 | + else { $effectiveRule.SourceAddressPrefix.Add($rule.SourceAddressPrefix) } |
| 161 | + } |
| 162 | + # Destination address prefix |
| 163 | + $effectiveRule.DestinationAddressPrefix = New-Object System.Collections.Generic.List[string] |
| 164 | + foreach ($prefix in $rule.DestinationAddressPrefix) { |
| 165 | + if ($prefix -eq "0.0.0.0/0") { $effectiveRule.DestinationAddressPrefix.Add("*"); break } |
| 166 | + else { $effectiveRule.DestinationAddressPrefix.Add($rule.DestinationAddressPrefix) } |
| 167 | + } |
| 168 | + $effectiveRule.Access = $rule.Access |
| 169 | + $effectiveRule.Priority = $rule.Priority |
| 170 | + $effectiveRule.Direction = $rule.Direction |
| 171 | + return $effectiveRule |
| 172 | +} |
| 173 | + |
| 174 | + |
| 175 | +function Get-NSGRules { |
| 176 | + param ( |
| 177 | + [Parameter(Position = 0)][ValidateNotNullOrEmpty()] |
| 178 | + [Microsoft.Azure.Commands.Compute.Models.PSVirtualMachine] $VM |
| 179 | + ) |
| 180 | + $effectiveNSG = Get-AzEffectiveNetworkSecurityGroup -NetworkInterfaceName ($VM.NetworkProfile.NetworkInterfaces.Id -Split '/')[-1] -ResourceGroupName $VM.ResourceGroupName -ErrorVariable NotAvailable -ErrorAction SilentlyContinue |
| 181 | + if ($NotAvailable) { |
| 182 | + # Not able to get effective rules so we'll construct them by hand |
| 183 | + $rules = @() |
| 184 | + # Get rules from NSG directly attached to the NIC |
| 185 | + $nic = Get-AzNetworkInterface | Where-Object { $_.Id -eq $VM.NetworkProfile.NetworkInterfaces.Id } |
| 186 | + $directNsgs = Get-AzNetworkSecurityGroup | Where-Object { $_.Id -eq $nic.NetworkSecurityGroup.Id } |
| 187 | + $directNsgRules = @() |
| 188 | + foreach ($directNsg in $directNsgs) { |
| 189 | + $directNsgRules = $directNsgRules + $directNsg.SecurityRules + $directNsg.DefaultSecurityRules |
| 190 | + } |
| 191 | + # Get rules from NSG attached to the subnet |
| 192 | + $subnetNsgs = Get-AzNetworkSecurityGroup | Where-Object { $_.Subnets.Id -eq $nic.IpConfigurations.Subnet.Id } |
| 193 | + $subnetNsgRules = @() |
| 194 | + foreach ($subnetNsg in $subnetNsgs) { |
| 195 | + $subnetNsgRules = $subnetNsgRules + $subnetNsg.SecurityRules + $subnetNsg.DefaultSecurityRules |
| 196 | + } |
| 197 | + $effectiveRules = @() |
| 198 | + if ($directNsgRules.Count -And $subnetNsgRules.Count) { |
| 199 | + Add-LogMessage -Level Warning "Found both NSG rules from both the NIC and the subnet for $($VM.Name). Evaluation of effective rules may be incorrect!" |
| 200 | + } |
| 201 | + # Convert each PSSecurityRule into a PSEffectiveSecurityRule |
| 202 | + foreach ($rule in ($directNsgRules + $subnetNsgRules)) { |
| 203 | + $effectiveRules = $effectiveRules + $(Convert-RuleToEffectiveRule $rule) |
| 204 | + } |
| 205 | + return $effectiveRules |
| 206 | + } else { |
| 207 | + $effectiveRules = $effectiveNSG.EffectiveSecurityRules |
| 208 | + # Sometimes the address prefix is retrieved as ("0.0.0.0/0", "0.0.0.0/0") rather than "*" (although these mean the same thing) |
| 209 | + foreach ($effectiveRule in $effectiveRules) { |
| 210 | + if ($effectiveRule.SourceAddressPrefix[0] -eq "0.0.0.0/0") { $effectiveRule.SourceAddressPrefix.Clear(); $effectiveRule.SourceAddressPrefix.Add("*") } |
| 211 | + if ($effectiveRule.DestinationAddressPrefix[0] -eq "0.0.0.0/0") { $effectiveRule.DestinationAddressPrefix.Clear(); $effectiveRule.DestinationAddressPrefix.Add("*") } |
| 212 | + } |
| 213 | + return $effectiveRules |
| 214 | + } |
| 215 | +} |
| 216 | + |
| 217 | +# Get original context before switching subscription |
| 218 | +# -------------------------------------------------- |
| 219 | +$originalContext = Get-AzContext |
| 220 | + |
| 221 | + |
| 222 | +# Load configuration from a benchmark subscription or config |
| 223 | +# ---------------------------------------------------------- |
| 224 | +if ($BenchmarkSubscription) { |
| 225 | + $JsonConfig = [ordered]@{} |
| 226 | + # Get VMs in current subscription |
| 227 | + $_ = Set-AzContext -SubscriptionId $BenchmarkSubscription |
| 228 | + $benchmarkVMs = Get-AzVM | Where-Object { $_.Name -NotLike "*shm-deploy*" } |
| 229 | + Add-LogMessage -Level Info "Found $($benchmarkVMs.Count) VMs in subscription: '$BenchmarkSubscription'" |
| 230 | + foreach ($VM in $benchmarkVMs) { |
| 231 | + Add-LogMessage -Level Info "... $($VM.Name)" |
| 232 | + } |
| 233 | + # Get the NSG rules and connectivity for each VM in the subscription |
| 234 | + foreach ($benchmarkVM in $benchmarkVMs) { |
| 235 | + Add-LogMessage -Level Info "Getting NSG rules and connectivity for $($VM.Name)" |
| 236 | + $JsonConfig[$benchmarkVM.Name] = [ordered]@{ |
| 237 | + InternetFromPort = [ordered]@{ |
| 238 | + "80" = (Test-OutboundConnection -VM $benchmarkVM -DestinationAddress "google.com" -DestinationPort 80) |
| 239 | + "443" = (Test-OutboundConnection -VM $benchmarkVM -DestinationAddress "google.com" -DestinationPort 443) |
| 240 | + } |
| 241 | + Rules = Get-NSGRules -VM $benchmarkVM |
| 242 | + } |
| 243 | + } |
| 244 | + $OutputFile = New-TemporaryFile |
| 245 | + Out-File -FilePath $OutputFile -Encoding "UTF8" -InputObject ($JsonConfig | ConvertTo-Json -Depth 10) |
| 246 | + Add-LogMessage -Level Info "Configuration file generated at '$($OutputFile.FullName)'" |
| 247 | + $BenchmarkJsonPath = $OutputFile.FullName |
| 248 | +} elseif ($BenchmarkConfig) { |
| 249 | + $BenchmarkJsonPath = $BenchmarkConfig |
| 250 | +} |
| 251 | + |
| 252 | + |
| 253 | +# Deserialise VMs from JSON config |
| 254 | +# -------------------------------- |
| 255 | +$BenchmarkJsonConfig = Get-Content -Path $BenchmarkJsonPath -Raw -Encoding UTF-8 | ConvertFrom-Json |
| 256 | +$benchmarkVMs = @() |
| 257 | +foreach ($JsonVm in $BenchmarkJsonConfig.PSObject.Properties) { |
| 258 | + $VM = New-Object -TypeName PsObject |
| 259 | + $VM | Add-Member -MemberType NoteProperty -Name Name -Value $JsonVm.Name |
| 260 | + $VM | Add-Member -MemberType NoteProperty -Name InternetFromPort -Value @{} |
| 261 | + $VM.InternetFromPort.80 = $JsonVm.PSObject.Properties.Value.InternetFromPort.80 |
| 262 | + $VM.InternetFromPort.443 = $JsonVm.PSObject.Properties.Value.InternetFromPort.443 |
| 263 | + $VM | Add-Member -MemberType NoteProperty -Name Rules -Value @() |
| 264 | + foreach ($rule in $JsonVm.PSObject.Properties.Value.Rules) { |
| 265 | + if ($rule.Name) { $VM.Rules += $(Convert-RuleToEffectiveRule $rule) } |
| 266 | + } |
| 267 | + $benchmarkVMs += $VM |
| 268 | +} |
| 269 | + |
| 270 | + |
| 271 | +# Get VMs in test SHM |
| 272 | +# ------------------- |
| 273 | +$_ = Set-AzContext -SubscriptionId $Subscription |
| 274 | +$testVMs = Get-AzVM |
| 275 | +Add-LogMessage -Level Info "Found $($testVMs.Count) VMs in subscription: '$Subscription'" |
| 276 | +foreach ($VM in $testVMs) { |
| 277 | + Add-LogMessage -Level Info "... $($VM.Name)" |
| 278 | +} |
| 279 | + |
| 280 | + |
| 281 | +# Create a hash table which maps test VMs to benchmark ones |
| 282 | +# --------------------------------------------------------- |
| 283 | +$vmHashTable = @{} |
| 284 | +foreach ($testVM in $testVMs) { |
| 285 | + $nameToCheck = $testVM.Name |
| 286 | + # Override matches for names that would otherwise fail |
| 287 | + if ($nameToCheck.StartsWith("CRAN-EXTERNAL-MIRROR")) { $nameToCheck = $nameToCheck.Replace("CRAN-EXTERNAL-MIRROR", "CRAN-MIRROR-EXTERNAL") } |
| 288 | + if ($nameToCheck.StartsWith("CRAN-INTERNAL-MIRROR")) { $nameToCheck = $nameToCheck.Replace("CRAN-INTERNAL-MIRROR", "CRAN-MIRROR-INTERNAL") } |
| 289 | + if ($nameToCheck.StartsWith("PYPI-EXTERNAL-MIRROR")) { $nameToCheck = $nameToCheck.Replace("PYPI-EXTERNAL-MIRROR", "PYPI-MIRROR-EXTERNAL") } |
| 290 | + if ($nameToCheck.StartsWith("PYPI-INTERNAL-MIRROR")) { $nameToCheck = $nameToCheck.Replace("PYPI-INTERNAL-MIRROR", "PYPI-MIRROR-INTERNAL") } |
| 291 | + if ($nameToCheck.StartsWith("APP-SRE")) { $nameToCheck = $nameToCheck.Replace("APP-SRE", "RDSSH1") } |
| 292 | + if ($nameToCheck.StartsWith("DKP-SRE")) { $nameToCheck = $nameToCheck.Replace("DKP-SRE", "RDSSH2") } |
| 293 | + # Only match against names that have not been matched yet |
| 294 | + $benchmarkVMNames = $benchmarkVMs | ForEach-Object { $_.Name } | Where-Object { ($vmHashTable.Values | ForEach-Object { $_.Name }) -NotContains $_ } |
| 295 | + $benchmarkVM = $benchmarkVMs | Where-Object { $_.Name -eq $(Select-ClosestMatch -Array $benchmarkVMNames -Value $nameToCheck) } |
| 296 | + $vmHashTable[$testVM] = $benchmarkVM |
| 297 | + Add-LogMessage -Level Info "matched $($testVM.Name) => $($benchmarkVM.Name)" |
| 298 | +} |
| 299 | + |
| 300 | +# Iterate over paired VMs checking their network settings |
| 301 | +# ------------------------------------------------------- |
| 302 | +foreach ($testVM in $testVMs) { |
| 303 | + $benchmarkVM = $vmHashTable[$testVM] |
| 304 | + |
| 305 | + # Get parameters for new VM |
| 306 | + # ------------------------- |
| 307 | + $_ = Set-AzContext -SubscriptionId $Subscription |
| 308 | + Add-LogMessage -Level Info "Getting NSG rules and connectivity for $($testVM.Name)" |
| 309 | + $testRules = Get-NSGRules -VM $testVM |
| 310 | + # Check that each NSG rule has a matching equivalent (which might be named differently) |
| 311 | + Add-LogMessage -Level Info "Comparing NSG rules for $($benchmarkVM.Name) and $($testVM.Name)" |
| 312 | + Add-LogMessage -Level Info "... ensuring that all $($benchmarkVM.Name) rules exist on $($testVM.Name)" |
| 313 | + Compare-NSGRules -BenchmarkRules $benchmarkVM.Rules -TestRules $testRules |
| 314 | + Add-LogMessage -Level Info "... ensuring that all $($testVM.Name) rules exist on $($benchmarkVM.Name)" |
| 315 | + Compare-NSGRules -BenchmarkRules $testRules -TestRules $benchmarkVM.Rules |
| 316 | + |
| 317 | + # Check that internet connectivity is the same for matched VMs |
| 318 | + Add-LogMessage -Level Info "Comparing internet connectivity for $($benchmarkVM.Name) and $($testVM.Name)..." |
| 319 | + # Test internet access on ports 80 and 443 |
| 320 | + foreach ($port in (80, 443)) { |
| 321 | + $testInternet = Test-OutboundConnection -VM $testVM -DestinationAddress "google.com" -DestinationPort $port |
| 322 | + if ($benchmarkVM.InternetFromPort[$port] -eq $testInternet) { |
| 323 | + Add-LogMessage -Level Success "The internet is '$($benchmarkVM.InternetFromPort[$port])' on port $port from both" |
| 324 | + } else { |
| 325 | + Add-LogMessage -Level Failure "The internet is '$($benchmarkVM.InternetFromPort[$port])' on port $port from $($benchmarkVM.Name)" |
| 326 | + Add-LogMessage -Level Failure "The internet is '$($testInternet)' on port $port from $($testVM.Name)" |
| 327 | + } |
| 328 | + } |
| 329 | +} |
| 330 | + |
| 331 | + |
| 332 | +# Switch back to original subscription |
| 333 | +# ------------------------------------ |
| 334 | +$_ = Set-AzContext -Context $originalContext |
0 commit comments