Modules/Private/70-Connectivity.ps1
|
# Issue #30 — Disconnected / semi-connected discovery # # Provides a pre-run connectivity matrix that classifies the runner's access posture # (connected / semi-connected / disconnected) and lets each collector declare its # transport requirements so the runtime can skip collectors gracefully rather than # letting them fail mid-run with unhelpful errors. # ───────────────────────────────────────────────────────────────────────────── # Azure endpoint reachability probe # ───────────────────────────────────────────────────────────────────────────── function Test-RangerAzureConnectivity { <# .SYNOPSIS Probes whether the Azure management plane is reachable from the current runner. .OUTPUTS Boolean — $true if management.azure.com:443 is reachable within the timeout. #> param( [int]$TimeoutSeconds = 10 ) $azureEndpoint = 'management.azure.com' if (-not (Test-RangerCommandAvailable -Name 'Test-NetConnection')) { # On platforms without Test-NetConnection fall back to a TCP socket attempt try { $tcp = New-Object System.Net.Sockets.TcpClient $async = $tcp.BeginConnect($azureEndpoint, 443, $null, $null) $wait = $async.AsyncWaitHandle.WaitOne([System.TimeSpan]::FromSeconds($TimeoutSeconds)) if ($wait) { $tcp.EndConnect($async) $tcp.Close() return $true } $tcp.Close() return $false } catch { return $false } } try { $result = Test-NetConnection -ComputerName $azureEndpoint -Port 443 ` -InformationLevel Quiet -WarningAction SilentlyContinue -ErrorAction SilentlyContinue return [bool]$result } catch { return $false } } # ───────────────────────────────────────────────────────────────────────────── # Per-target WinRM reachability probe (non-caching, used in matrix only) # ───────────────────────────────────────────────────────────────────────────── function Test-RangerTargetTcpReachability { <# .SYNOPSIS Tests whether a host responds on port 5985 or 5986 within the timeout. .OUTPUTS Ordered hashtable: { Reachable, Port, TransportHint } #> param( [Parameter(Mandatory = $true)] [string]$ComputerName, [int]$TimeoutSeconds = 10 ) foreach ($port in @(5985, 5986)) { $reachable = $false if (Test-RangerCommandAvailable -Name 'Test-NetConnection') { try { $r = Test-NetConnection -ComputerName $ComputerName -Port $port ` -InformationLevel Quiet -WarningAction SilentlyContinue -ErrorAction SilentlyContinue $reachable = [bool]$r } catch { $reachable = $false } } else { try { $tcp = New-Object System.Net.Sockets.TcpClient $async = $tcp.BeginConnect($ComputerName, $port, $null, $null) $wait = $async.AsyncWaitHandle.WaitOne([System.TimeSpan]::FromSeconds($TimeoutSeconds)) if ($wait) { $tcp.EndConnect($async); $reachable = $true } $tcp.Close() } catch { $reachable = $false } } if ($reachable) { return [ordered]@{ Reachable = $true Port = $port TransportHint = if ($port -eq 5986) { 'https' } else { 'http' } } } } return [ordered]@{ Reachable = $false Port = $null TransportHint = $null } } # ───────────────────────────────────────────────────────────────────────────── # Full connectivity matrix # ───────────────────────────────────────────────────────────────────────────── function Get-RangerConnectivityMatrix { <# .SYNOPSIS Probes all transport surfaces (cluster WinRM, Azure management plane, BMC HTTPS) and returns a structured matrix for the current runner. .DESCRIPTION Called once before collectors run. Results are stored in manifest.run.connectivity and passed to Invoke-RangerCollectorExecution so collectors can skip rather than fail when their required transport is unavailable. .OUTPUTS Ordered hashtable: { posture # 'connected' | 'semi-connected' | 'disconnected' probeTimeUtc # ISO-8601 timestamp cluster # { reachable, targets[] } azure # { reachable, endpoint } bmc # { reachable, endpoints[] } arc # { available } — Arc transport feasibility (set later by #26) } #> param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Config, [int]$TimeoutSeconds = 10 ) $probeTime = (Get-Date).ToUniversalTime().ToString('o') $clusterReachable = $false $clusterTargetResults = New-Object System.Collections.ArrayList # — Cluster WinRM probes — $clusterTargets = [System.Collections.Generic.List[string]]::new() if (-not [string]::IsNullOrWhiteSpace([string]$Config.targets.cluster.fqdn) -and -not (Test-RangerPlaceholderValue -Value $Config.targets.cluster.fqdn -FieldName 'targets.cluster.fqdn')) { $clusterTargets.Add([string]$Config.targets.cluster.fqdn) } foreach ($node in @($Config.targets.cluster.nodes)) { if (-not [string]::IsNullOrWhiteSpace([string]$node) -and -not (Test-RangerPlaceholderValue -Value $node -FieldName 'targets.cluster.node') -and $node -notin $clusterTargets) { $clusterTargets.Add([string]$node) } } foreach ($target in $clusterTargets) { $probe = Test-RangerTargetTcpReachability -ComputerName $target -TimeoutSeconds $TimeoutSeconds [void]$clusterTargetResults.Add([ordered]@{ target = $target reachable = $probe.Reachable port = $probe.Port transport = $probe.TransportHint }) if ($probe.Reachable) { $clusterReachable = $true } } # — Azure management plane probe — $azureEnabled = (-not [string]::IsNullOrWhiteSpace([string]$Config.targets.azure.subscriptionId) -and -not (Test-RangerPlaceholderValue -Value $Config.targets.azure.subscriptionId -FieldName 'targets.azure.subscriptionId')) $azureReachable = $false if ($azureEnabled) { $azureReachable = Test-RangerAzureConnectivity -TimeoutSeconds $TimeoutSeconds } # — BMC HTTPS probes — $bmcReachable = $false $bmcEndpointResults = New-Object System.Collections.ArrayList foreach ($ep in @($Config.targets.bmc.endpoints)) { $bmcHost = if ($ep -is [System.Collections.IDictionary]) { [string]$ep['host'] } else { [string]$ep } if ([string]::IsNullOrWhiteSpace($bmcHost)) { continue } $probe = $false if (Test-RangerCommandAvailable -Name 'Test-NetConnection') { try { $probe = [bool](Test-NetConnection -ComputerName $bmcHost -Port 443 ` -InformationLevel Quiet -WarningAction SilentlyContinue -ErrorAction SilentlyContinue) } catch { $probe = $false } } else { try { $tcp = New-Object System.Net.Sockets.TcpClient $async = $tcp.BeginConnect($bmcHost, 443, $null, $null) $wait = $async.AsyncWaitHandle.WaitOne([System.TimeSpan]::FromSeconds($TimeoutSeconds)) if ($wait) { $tcp.EndConnect($async); $probe = $true } $tcp.Close() } catch { $probe = $false } } [void]$bmcEndpointResults.Add([ordered]@{ host = $bmcHost; reachable = $probe }) if ($probe) { $bmcReachable = $true } } # — Posture classification — $posture = switch ($true) { ($clusterReachable -and $azureReachable) { 'connected'; break } ($clusterReachable -and -not $azureReachable -and $azureEnabled) { 'semi-connected'; break } ($clusterReachable -and -not $azureEnabled) { 'connected'; break } # Azure not configured — not a degradation default { 'disconnected' } } [ordered]@{ posture = $posture probeTimeUtc = $probeTime cluster = [ordered]@{ reachable = $clusterReachable targets = @($clusterTargetResults) } azure = [ordered]@{ reachable = $azureReachable enabled = $azureEnabled endpoint = 'management.azure.com:443' } bmc = [ordered]@{ reachable = $bmcReachable endpoints = @($bmcEndpointResults) } arc = [ordered]@{ available = $false # updated by Arc transport init in 40-Execution.ps1 (#26) } } } # ───────────────────────────────────────────────────────────────────────────── # Collector dependency resolution # ───────────────────────────────────────────────────────────────────────────── function Test-RangerCollectorConnectivitySatisfied { <# .SYNOPSIS Returns $true if the connectivity matrix satisfies the transport requirements of the given collector definition. .DESCRIPTION Used by Invoke-RangerCollectorExecution to decide whether to skip a collector rather than attempt a run that will fail. Transport requirements by RequiredCredential: 'cluster' → needs cluster.reachable = $true 'azure' → needs azure.reachable = $true 'bmc' → needs bmc.reachable = $true (only relevant when endpoints configured) 'none' → always satisfied #> param( [Parameter(Mandatory = $true)] [object]$Definition, [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$ConnectivityMatrix ) switch ($Definition.RequiredCredential) { 'cluster' { # Only skip when we actually probed targets and found them unreachable. # If targets list is empty (all placeholders or none configured) we cannot # assert unreachability — let the collector proceed and self-determine N/A. if (@($ConnectivityMatrix.cluster.targets).Count -eq 0) { return $true } return [bool]$ConnectivityMatrix.cluster.reachable } 'azure' { # Only skip when Azure is explicitly configured AND the probe confirmed unreachable. # When azure is not configured (placeholder subscriptionId), enabled = $false; # pass through so the collector can return 'not-applicable' rather than being silently skipped. if (-not $ConnectivityMatrix.azure.enabled) { return $true } return [bool]$ConnectivityMatrix.azure.reachable } 'bmc' { # BMC is optional; skip gracefully when no endpoints are configured or reachable. if (-not $ConnectivityMatrix.bmc.endpoints -or @($ConnectivityMatrix.bmc.endpoints).Count -eq 0) { return $false # no endpoints configured → skip } return [bool]$ConnectivityMatrix.bmc.reachable } 'none' { return $true } default { return $true } } } # ───────────────────────────────────────────────────────────────────────────── # Connectivity finding factory # ───────────────────────────────────────────────────────────────────────────── function New-RangerConnectivityFinding { <# .SYNOPSIS Creates a structured finding for a connectivity gap discovered during the matrix probe. #> param( [Parameter(Mandatory = $true)] [ValidateSet('cluster', 'azure', 'bmc')] [string]$Surface, [string]$Detail ) $titles = @{ cluster = 'Cluster WinRM targets unreachable from runner' azure = 'Azure management plane unreachable — Azure-dependent collectors skipped' bmc = 'BMC endpoints unreachable — hardware collector skipped' } $recs = @{ cluster = 'Verify network routing and WinRM firewall rules between the runner and cluster nodes. Use Test-NetConnection to diagnose port reachability.' azure = 'Confirm the runner has outbound HTTPS to management.azure.com. In disconnected environments, use Arc Run Command transport or pre-collect Azure data offline.' bmc = 'Confirm BMC endpoint IPs are reachable on port 443 from the runner and that iDRAC/iLO HTTPS is enabled.' } New-RangerFinding -Severity warning ` -Title $titles[$Surface] ` -Description "Connectivity probe detected that the $Surface surface is unreachable." ` -CurrentState ($Detail ?? 'probe timed out or TCP refused') ` -Recommendation $recs[$Surface] } |