modules/AzStack.Insights/analyzers/AzStackHci.DiagnosticSettings.NetworkConnectivity.ps1
|
<#
.SYNOPSIS Uses the AzStackHci.DiagnosticSettings module and Test-AzureLocalConnectivity function to test connectivity to required endpoints for Azure Local. Data is also collected from the Arc for Servers Agent on each node. The results of the connectivity tests are then evaluated in the associated rules, which set the status of the analyzer accordingly. .DESCRIPTION DESIGN NOTES (orchestration overview - the inline comments below carry the per-statement detail): 1. Per-node, local-only execution. The Insights framework already fans this analyzer out to every cluster node, so every signal collected here is LOCAL to the current node - there is NO cross-node remoting and no PowerShell double-hop. Each node reports its own view; aggregation happens upstream in the framework. 2. Strategy: run-and-parse. Test-AzureLocalConnectivity is invoked once (quiet/offline-friendly: -NoAutoUpdate -NoOutput, with -PassThru) and its result object - including the module's run-level telemetry and SSL / Private-Link state - is handed to the rules. The module is the source of truth; this analyzer does not re-classify endpoints itself. 3. Best-effort node signals (none are fatal). Three local probes enrich the per-node report but must never abort the connectivity sweep - a missing module/CLI or an error is logged as a Warning and execution continues: Get-AzureStackHCI (cloud registration status + Azure region), azcmagent (Arc for Servers agent status + Azure identity), and Test-ArcMachinePrivateLinkScopeEnabled (Arc Private Link Scope state). 4. Telemetry property report shape. Run-level telemetry is emitted as a TALL Section | Metric | Value table (a single property report, honouring the framework's single-report analyzer contract) so it never causes horizontal scroll in the HTML report and scales as fields are added. 5. StrictMode safety. Variables read later on multiple code paths (e.g. $ConnectionStatus, $arcAgentStatus, $arcAgentDetails, $AzureStackHCIInfo) are initialised to $null up-front so no path reads an unset variable. #> [CmdletBinding()] param( [Parameter(Mandatory=$false)] [ValidateScript({if($_.ToString().Length -lt 25 -or $_.ToString().Length -gt 255) { # 25 allows for https:// and the domain name # 255 is the maximum length for a KeyVault URL # Have to use .ToString() to get the length of the URL, as $_ is a Uri object # Invalid length, show example using custom error message Throw "'$($_.ToString())' is not valid KeyVault URL endpoint, length = $($_.ToString().Length). Expected length is between 25 to 255 characters. Example parameter for KeyVault: 'https://yourhcikeyvaultname.vault.azure.net'" } else { # Valid URL length, return $True $True }})] [System.Uri]$KeyVaultURL, [Parameter(Mandatory=$false)] [String]$AzureRegion = $global:CSSTools_AzsSupport.EnvironmentInfo.Region, # HTTP request method forwarded to Test-AzureLocalConnectivity for every endpoint. # 'Auto' (the module default) issues an HTTP HEAD first and transparently falls back to GET on a 405/400/ambiguous-403 # for the offending endpoint only - this is the fastest safe option and is what we want for an Insights sweep. # 'Get' issues GET for every endpoint (use this if a proxy mishandles HEAD). # 'Head' is HEAD-only with no fallback (fastest, but some endpoints - e.g. storage SAS URLs - reject HEAD). [Parameter(Mandatory=$false)] [ValidateSet('Get','Head','Auto')] [String]$RequestMethod = 'Auto', # Number of process-isolated parallel workers for the Layer-7 endpoint sweep. # 1 = sequential. 2..16 partition the endpoint list round-robin across that many # Start-Job workers, cutting wall-clock time on a full sweep. We default to 8 to keep the analyzer responsive on the # large Azure Local endpoint set while staying within the module's ValidateRange(1,16). Output ordering is preserved # (the module re-sorts results by RowID at merge), so rule evaluation is unaffected by the chosen parallelism. [Parameter(Mandatory=$false)] [ValidateRange(1,16)] [Int]$Parallelism = 8, # NTP time server to probe on UDP/123 instead of the module default 'time.windows.com'. Forwarded to # Test-AzureLocalConnectivity (-NTPTimeServer), which swaps the time.windows.com row for this server and skips # the RFC1918 / Private-Link check for it (an internal/domain time source is expected to resolve privately). # When NOT supplied we default to the Active Directory domain ($env:USERDNSDOMAIN) - the actual time source on # a domain-joined Azure Local node (w32time NT5DS) - if that environment variable is available; otherwise the # parameter is left unset and the module falls back to testing 'time.windows.com'. [Parameter(Mandatory=$false)] [string]$NTPTimeServer, # Arc Gateway URL for an Arc-Gateway-deployed cluster (form: https://<guid>.gw.arc.azure.com or .net). When set # - or auto-detected from the local Arc agent (connection.type='gateway' + the 'Gateway URL' field) - it is forwarded to # Test-AzureLocalConnectivity, which switches into Arc-Gateway mode: gateway-SUPPORTED endpoints are reached # THROUGH the gateway and skipped from the direct-egress test (ResultCategory='Skipped'), and only the endpoints # that do NOT support Arc Gateway - plus the gateway URL itself - are validated directly. When neither supplied # nor detected, the module tests the full endpoint set directly (non-gateway behaviour). An explicit value here # overrides auto-detection. [Parameter(Mandatory=$false)] [System.Uri]$ArcGatewayURL ) # Guarded runtime import (parse-time `using module` triggers heavy startup that fails on clean checkouts). if (-not (Get-Command -Name 'Initialize-InsightAnalyzer' -ErrorAction SilentlyContinue)) { Import-Module $PSScriptRoot\..\AzStack.Insights.psm1 -Force -DisableNameChecking } # Import localization strings from the locale directory # this will natively import the psd1 file that has the same name as the script # and the locale folder that matches the current culture of the user running the script Import-LocalizedData -BindingVariable 'localizedData' -BaseDirectory "$PSScriptRoot\locale" -UICulture $PSUICulture $insightAnalyzer = Initialize-InsightAnalyzer -Id $localizedData.Id -Properties $localizedData.Insight <# START ANALYZER LOGIC #> try { # Initialise both node-connectivity signals up-front so they are always defined before they are read in the # telemetry property report and the Azure connection-status rule below. Keeping them initialised here makes the # script safe under Set-StrictMode and avoids reading an uninitialised variable on any code path. # $ConnectionStatus -> Azure Local cloud registration status (Get-AzureStackHCI ConnectionStatus) # $arcAgentStatus -> Arc for Servers agent status (azcmagent show) $ConnectionStatus = $null $arcAgentStatus = $null $arcAgentDetails = $null # Arc Gateway hostname auto-detected from the local Arc agent (empty on a non-gateway cluster). Read later when # resolving whether to forward -ArcGatewayURL to Test-AzureLocalConnectivity; initialised here for StrictMode. $detectedArcGatewayUrl = '' # Arc agent connection type ('direct' | 'default' | 'gateway'); 'gateway' is the authoritative signal that the # Arc Gateway is enabled on this node. Initialised here for StrictMode; populated from azcmagent below. $arcConnectionType = '' # Initialised $AzureStackHCIInfo so the StrictMode-safe `if($AzureStackHCIInfo)` guard below never reads an unset # variable on the path where Get-AzureStackHCI throws a terminating error before the assignment completes. $AzureStackHCIInfo = $null if($Global:CSSTools_AzsSupport.EnvironmentInfo.DisconnectedOps) { # Disconnected Operations (air-gapped) clusters cannot reach the public Azure endpoints this analyzer # probes, so the whole connectivity sweep is meaningless here. Throw NotSupportedException BEFORE any # collection runs - the catch block below recognises this exception type and sets the analyzer status to # NOT_APPLICABLE (a graceful skip), rather than letting it surface as an UNKNOWN/error. throw New-Object System.NotSupportedException("This analyzer does not support disconnected operations at this time.") } # Always query the local Azure Stack HCI configuration for this node's cloud registration (ConnectionStatus) # and - when not explicitly supplied - the Azure region. ConnectionStatus is needed both for the run-level # telemetry column and for the Azure connection-status rule below, so this call is no longer gated behind the # "-AzureRegion not supplied" branch (which is now skipped because AzureRegion defaults from the CSSTools # global). The call is local to this node; the Insights framework already fans this analyzer out to every # cluster node (for example -ComputerName (Get-ClusterNode).Name), so each node reports its own status and no # cross-node remoting (double-hop) is performed here. try { try { # Get-AzureStackHCI is a local cmdlet that reads the cluster's registration state and returns a single object with ConnectionStatus, Region, and other properties. It may throw if the module is not present or if the cluster is not registered. We catch any errors and log them, but do not fail the analyzer; we still want to run the connectivity tests even if this call fails. $AzureStackHCIInfo = Get-AzureStackHCI -ErrorAction SilentlyContinue } catch { Write-InsightLog -Level 'Warning' -Message "Get-AzureStackHCI failed: $($_.Exception.Message)" } # If the call succeeded, extract the ConnectionStatus and Region properties. If the call failed or returned no object, log a warning and leave the variables as $null. The ConnectionStatus is used for the Azure connection-status rule, and the Region is used for the connectivity tests. If the Region is not supplied and cannot be retrieved, we throw an error to indicate that the user must specify it. if($AzureStackHCIInfo) { # ConnectionStatus is a string that can be 'Connected', 'Disconnected', or 'NotRegistered'. We store it in the $ConnectionStatus variable for use in the Azure connection-status rule. The Region is also stored in $AzureRegion if it was not explicitly supplied. $ConnectionStatus = $AzureStackHCIInfo.ConnectionStatus } else { Write-InsightLog -Level 'Warning' -Message "Get-AzureStackHCI returned no information; cloud registration status is unavailable on this node." } Write-InsightLog -Message "Retrieved cloud registration status '$ConnectionStatus' from local Azure Stack HCI configuration." if ((-not $AzureRegion) -and $AzureStackHCIInfo) { $AzureRegion = $AzureStackHCIInfo.Region Write-InsightLog -Message "Retrieved Azure region '$AzureRegion' from local Azure Stack HCI configuration." } } catch { Write-InsightLog -Level 'Warning' -Message "Unable to retrieve cloud registration status / region from local Azure Stack HCI configuration: $($_.Exception.Message)" if (-not $AzureRegion) { throw "Azure region not specified and could not be retrieved from local configuration. Please specify the AzureRegion parameter." } } # Query the local Arc for Servers agent (azcmagent) for its own connection status. This is the Arc agent's # view of Azure connectivity (Connected / Disconnected / Expired), distinct from the HCI cloud registration # status above. Best-effort only: a missing agent or CLI error must not fail the connectivity sweep. try { if (Get-Command -Name 'azcmagent' -ErrorAction SilentlyContinue) { $arcAgentData = azcmagent show -j 2>$null | ConvertFrom-Json $arcAgentStatus = if ($arcAgentData -and $arcAgentData.PSObject.Properties['status']) { [string]$arcAgentData.status } else { $null } Write-InsightLog -Message "Retrieved Arc for Servers agent status '$arcAgentStatus' from local azcmagent." # Authoritative Arc Gateway signal: 'azcmagent config get connection.type' returns 'direct', 'default', # or 'gateway'. 'gateway' = the Arc Gateway is ENABLED on this node; 'default' (the install state, which # the cloud can switch to gateway) and 'direct' (explicitly opted out) both mean it is NOT using the # gateway. Best-effort: older agents may not expose it, in which case we fall back to the URL heuristic. try { $arcConnectionType = (azcmagent config get connection.type 2>$null | Out-String).Trim() if ($arcConnectionType) { Write-InsightLog -Message "Arc agent connection.type = '$arcConnectionType'." } } catch { Write-InsightLog -Level 'Warning' -Message "Unable to read Arc agent connection.type from azcmagent: $($_.Exception.Message)" } # Enrich the per-node output with the Arc machine's Azure identity and key agent fields. The JSON # already exposes subscriptionId / tenantId / resourceGroup directly; we still parse the resourceId # as a fallback in case a field is blank. resourceId is a single string, so -match safely populates # $Matches here. All fields are best-effort and informational only. if ($arcAgentData) { $arcResourceId = if ($arcAgentData.PSObject.Properties['resourceId']) { [string]$arcAgentData.resourceId } else { '' } $arcSubscriptionId = if ($arcAgentData.PSObject.Properties['subscriptionId']) { [string]$arcAgentData.subscriptionId } else { '' } $arcResourceGroup = if ($arcAgentData.PSObject.Properties['resourceGroup']) { [string]$arcAgentData.resourceGroup } else { '' } if ((-not $arcSubscriptionId) -and $arcResourceId -match '/subscriptions/([^/]+)/') { $arcSubscriptionId = $Matches[1] } if ((-not $arcResourceGroup) -and $arcResourceId -match '/resourceGroups/([^/]+)/') { $arcResourceGroup = $Matches[1] } # Detect the Arc Gateway URL - populated only on an Arc-Gateway-deployed node (the azcmagent 'Gateway # URL' value). connection.type tells us the gateway is ENABLED, but the module needs the actual # endpoint to forward as -ArcGatewayURL. Prefer the direct 'gatewayUrl' JSON field; fall back to a # value-scan for the distinctive gateway hostname (<guid>.gw.arc.azure.com/.net) across every field, # since the JSON key name has varied across Arc agent versions. if ($arcAgentData.PSObject.Properties['gatewayUrl'] -and $arcAgentData.gatewayUrl) { $detectedArcGatewayUrl = [string]$arcAgentData.gatewayUrl } else { foreach ($arcProp in $arcAgentData.PSObject.Properties) { if ([string]$arcProp.Value -match '(?i)[a-f0-9-]{36}\.gw\.arc\.azure\.(?:com|net)') { $detectedArcGatewayUrl = $Matches[0] break } } } if ($detectedArcGatewayUrl) { Write-InsightLog -Message "Detected Arc Gateway URL '$detectedArcGatewayUrl' from the local Arc agent; the cluster appears to be Arc-Gateway deployed." } $arcAgentDetails = [PSCustomObject]@{ ArcResourceName = if ($arcAgentData.PSObject.Properties['resourceName']) { [string]$arcAgentData.resourceName } else { '' } ArcResourceGroup = $arcResourceGroup ArcSubscriptionId = $arcSubscriptionId ArcTenantId = if ($arcAgentData.PSObject.Properties['tenantId']) { [string]$arcAgentData.tenantId } else { '' } ArcLocation = if ($arcAgentData.PSObject.Properties['location']) { [string]$arcAgentData.location } else { '' } ArcCloud = if ($arcAgentData.PSObject.Properties['cloud']) { [string]$arcAgentData.cloud } else { '' } ArcCloudProvider = if ($arcAgentData.PSObject.Properties['cloudProvider']) { [string]$arcAgentData.cloudProvider } else { '' } ArcAgentVersion = if ($arcAgentData.PSObject.Properties['agentVersion']) { [string]$arcAgentData.agentVersion } else { '' } ArcLastHeartbeat = if ($arcAgentData.PSObject.Properties['lastHeartbeat']) { [string]$arcAgentData.lastHeartbeat } else { '' } ArcAgentErrorCode = if ($arcAgentData.PSObject.Properties['agentErrorCode']){ [string]$arcAgentData.agentErrorCode }else { '' } ArcHttpsProxy = if ($arcAgentData.PSObject.Properties['httpsProxy']) { [string]$arcAgentData.httpsProxy } else { '' } ArcProxyBypass = if ($arcAgentData.PSObject.Properties['proxyBypass']) { [string]$arcAgentData.proxyBypass } else { '' } ArcGatewayUrl = $detectedArcGatewayUrl ArcConnectionType = $arcConnectionType ArcResourceId = $arcResourceId } } } else { Write-InsightLog -Level 'Warning' -Message "azcmagent CLI not found; Arc for Servers agent status is unavailable on this node." } } catch { Write-InsightLog -Level 'Warning' -Message "Unable to retrieve Arc for Servers agent status from azcmagent: $($_.Exception.Message)" } # Query whether this Arc machine is associated with an Azure Monitor Private Link Scope. The function is part # of the AzStackHci.DiagnosticSettings module, authenticates via Managed Identity, reads the Arc resource id # from the METRICS_ARC_RESOURCE_URI environment variable, and returns a single boolean. It emits verbose / # warning / host text that we suppress; only the boolean is captured. Informational only - a node may # legitimately use public endpoints (returns $false) - and best-effort, so any error is logged and ignored. $privateLinkScopeEnabled = $null try { if (Get-Command -Name 'Test-ArcMachinePrivateLinkScopeEnabled' -ErrorAction SilentlyContinue) { $privateLinkScopeResult = Test-ArcMachinePrivateLinkScopeEnabled -WarningAction SilentlyContinue -ErrorAction Stop $privateLinkScopeBool = @($privateLinkScopeResult) | Where-Object { $_ -is [bool] } | Select-Object -Last 1 if ($null -ne $privateLinkScopeBool) { $privateLinkScopeEnabled = [bool]$privateLinkScopeBool Write-InsightLog -Message "Arc Private Link Scope enabled on this node: $privateLinkScopeEnabled." } else { Write-InsightLog -Level 'Warning' -Message "Test-ArcMachinePrivateLinkScopeEnabled did not return a boolean result; Arc Private Link Scope state is unavailable on this node." } } else { Write-InsightLog -Level 'Warning' -Message "Test-ArcMachinePrivateLinkScopeEnabled not found; Arc Private Link Scope state is unavailable on this node." } } catch { Write-InsightLog -Level 'Warning' -Message "Unable to determine Arc Private Link Scope state via Test-ArcMachinePrivateLinkScopeEnabled: $($_.Exception.Message)" } # Run Connectivity Test and get results to pass to rules # Fast-fail explicitly if the Azure region could not be determined. Get-AzureStackHCI above is called with # -ErrorAction SilentlyContinue (so it never throws a terminating error), which means the earlier catch-based # guard does NOT fire when the cmdlet simply returns nothing. Without this deterministic check the sweep would # proceed with an empty AzureRegion. This throw is caught by the analyzer's outer catch and surfaced as UNKNOWN. if (-not $AzureRegion) { throw "Azure region not specified and could not be retrieved from local configuration. Please specify the AzureRegion parameter." } Write-InsightLog -Message "Testing connectivity to Azure Local endpoints for region '$AzureRegion' from machine '$env:COMPUTERNAME' with connection status '$ConnectionStatus', this could take a one to two minutes..." # Build base parameters for Test-AzureLocalConnectivity; conditionally add KeyVaultURL if provided. # NoAutoUpdate/NoOutput keep the run quiet and offline-cache friendly; PassThru returns the 0.6.8 structured # object (run-level telemetry + SSL/Private-Link/proxy/CRL state as top-level properties, endpoint rows under # .Results) for the rules to evaluate. # RequestMethod/Parallelism are forwarded from this analyzer's parameters (defaults: Auto / 8). # Write the detailed connectivity report INTO this node's own report folder - the SAME per-node folder the # Insights framework builds and copies back to the orchestrator: <OutputDirectory>\<COMPUTERNAME>\. We anchor to # the framework's resolved OutputDirectory (exposed on the Insights cache) rather than reconstructing it from # Get-AzsSupportWorkingDirectory: on a REMOTE node the framework uses the orchestrator-supplied OutputDirectory, # which differs from the remote node's own (per-session, timestamped) working directory - so reconstructing it # would write the report to a divergent folder that is never collected. Falling back to the working directory # keeps direct/Pester invocations working. We replicate Export-AzsSupportInsightToHtml's exact folder-name # transform (ToUpper() with whitespace -> '_') so the report lands inside the collected node folder and its # relative ReportLinks anchor resolves in the bundle. $insightOutputDir = if ($Global:AzStack_Insights.Cache.OutputDirectory) { $Global:AzStack_Insights.Cache.OutputDirectory } else { Join-Path (Get-AzsSupportWorkingDirectory) 'InsightReport' } $nodeReportFolder = (Join-Path $insightOutputDir $env:COMPUTERNAME.ToUpper()) -replace '\s', '_' $ExportPath = Join-Path $nodeReportFolder 'AzLocalConnectivityTests' [hashtable]$connectivityParams = @{ AzureRegion = $AzureRegion RequestMethod = $RequestMethod Parallelism = $Parallelism NoAutoUpdate = $true NoOutput = $true ForceGitHubEndpointsUpdate = $true PassThru = $true ExportPath = $ExportPath ErrorAction = 'Stop' ExcludeUploadResults = $true # ********************************************************************************************************************** # **** -Scope Cluster is NOT required, as Insights already runs per-node and aggregates upstream. # Only enable this deliberately if you want a single invocation to fan out across every cluster node via Invoke-Command # and return a merged cluster-shaped result. See the comments below for important caveats before enabling. # ********************************************************************************************************************** # --- Optional: cluster-wide sweep (AzStackHci.DiagnosticSettings v0.6.7+) ----------------------- # By default this analyzer runs node-scoped (the module default Scope = 'Node'), because Insights # already executes per-node and aggregates upstream. If you instead want a SINGLE invocation to fan # out across every cluster node via Invoke-Command and return a merged cluster-shaped result, add the # commented-out line below so you can switch between Node and Cluster as needed. # # IMPORTANT before enabling -Scope Cluster: # * The module must be present at the SAME version on every node, and standard Kerberos remoting # must work (no CredSSP / TrustedHosts changes are made by the module). # * -Scope Cluster changes the RETURN SHAPE to a cluster pscustomobject (per-node results under a # .Nodes map), NOT the per-node ArrayList the rules below expect - so the downstream rule # evaluation would need to be adapted (e.g. iterate $result.Nodes) before using this in production. # # Scope = 'Cluster' # # InstallMissingModuleOnNodes pairs with -Scope Cluster: when a remote node does not already have # AzStackHci.DiagnosticSettings, the module installs/sideloads it on that node before testing. Leave this # OFF by default - it MUTATES remote node state and conflicts with this repo's policy of not installing # packages on the fly and pinning module versions. Only enable it deliberately, in an environment where # auto-provisioning the module on every cluster node is acceptable and you have verified version parity. # Check with Adam Rudell if this would be needed for AzureLocal-CSSTools or not, probably not, but could # cause failures if a node is missing the module and this is not set. # If we enable it, make sure to test in a safe environment first. # # InstallMissingModuleOnNodes = $true } if ($KeyVaultURL) { Write-InsightLog -Message "KeyVaultURL provided: '$KeyVaultURL'. This will be included in connectivity tests and surfaced in results." $connectivityParams['KeyVaultURL'] = $KeyVaultURL } else { Write-InsightLog -Message "KeyVaultURL not provided, connectivity tests will proceed without it." } # Resolve the NTP time server for the UDP/123 reachability test. An explicit -NTPTimeServer wins. Otherwise, on # a domain-joined node, default to the AD domain ($env:USERDNSDOMAIN) - the node's real time source (NT5DS) - so # the probe targets the internal time server rather than 'time.windows.com' (which on some networks resolves to # an internal appliance and would otherwise trip the RFC1918 / Private-Link check). When neither is available the # parameter is left unset and the module falls back to its 'time.windows.com' default. if ([string]::IsNullOrWhiteSpace($NTPTimeServer) -and -not [string]::IsNullOrWhiteSpace($env:USERDNSDOMAIN)) { $NTPTimeServer = $env:USERDNSDOMAIN Write-InsightLog -Message "NTPTimeServer not supplied; defaulting to the AD domain '$NTPTimeServer' (from `$env:USERDNSDOMAIN) for the NTP (UDP/123) reachability test." } # Forward the resolved NTP time server only when it is set AND the installed Test-AzureLocalConnectivity exposes # the parameter (0.6.x+), so an older module that lacks it does not fail parameter binding (which would end the # analyzer as UNKNOWN). When unset, the module keeps its 'time.windows.com' default. if (-not [string]::IsNullOrWhiteSpace($NTPTimeServer)) { $connectivityCommand = Get-Command -Name 'Test-AzureLocalConnectivity' -ErrorAction SilentlyContinue if ($connectivityCommand -and $connectivityCommand.Parameters.ContainsKey('NTPTimeServer')) { $connectivityParams['NTPTimeServer'] = $NTPTimeServer Write-InsightLog -Message "Using NTP time server '$NTPTimeServer' for the connectivity NTP (UDP/123) test." } else { Write-InsightLog -Level 'Warning' -Message "The installed Test-AzureLocalConnectivity does not expose -NTPTimeServer; falling back to the module default ('time.windows.com') for the NTP test." } } # Resolve the Arc Gateway URL for this sweep. An explicit -ArcGatewayURL wins; otherwise use the value # auto-detected from the local Arc agent above ($detectedArcGatewayUrl). Normalise to include the https:// # scheme (the module requires it and validates the length 60-62). Forwarding -ArcGatewayURL switches the module # into Arc-Gateway mode - it routes the gateway-SUPPORTED endpoints through the gateway (skipping their # direct-egress test, ResultCategory='Skipped') and validates only the non-gateway endpoints plus the gateway # URL itself. The module auto-enables -ArcGatewayDeployment when -ArcGatewayURL is supplied, so we only pass the # URL. # Choose the Arc Gateway URL to forward. An explicit -ArcGatewayURL always wins. Otherwise auto-detect, but let # the authoritative connection.type veto a false positive: only auto-enable when connection.type is 'gateway' # (or is unavailable on an older agent, in which case we trust the detected URL alone). When connection.type is # 'direct' / 'default' the node is NOT on the gateway, so we ignore any detected hostname. if ($ArcGatewayURL) { $arcGatewayUrlToUse = [string]$ArcGatewayURL } elseif ($detectedArcGatewayUrl -and $arcConnectionType -and ($arcConnectionType -inotmatch 'gateway')) { $arcGatewayUrlToUse = '' Write-InsightLog -Message "Arc agent connection.type is '$arcConnectionType' (not 'gateway'); not auto-enabling Arc Gateway mode despite a detected gateway hostname." } else { $arcGatewayUrlToUse = $detectedArcGatewayUrl } # Surface the odd state where the gateway is enabled but its URL could not be determined from the agent. if ((-not $ArcGatewayURL) -and (-not $arcGatewayUrlToUse) -and ($arcConnectionType -imatch 'gateway')) { Write-InsightLog -Level 'Warning' -Message "Arc agent connection.type is 'gateway' but the gateway URL could not be determined from azcmagent; testing the full endpoint set directly. Supply -ArcGatewayURL to force gateway mode." } $arcGatewayModeEnabled = $false if (-not [string]::IsNullOrWhiteSpace($arcGatewayUrlToUse)) { if ($arcGatewayUrlToUse -notmatch '^(?i)https?://') { $arcGatewayUrlToUse = "https://$arcGatewayUrlToUse" } # Only forward when the value is well-formed (the module's ValidateScript expects the exact Arc Gateway form # and length 60-62 - forwarding a malformed value would throw a binding error and end the analyzer as # UNKNOWN) AND the installed Test-AzureLocalConnectivity exposes -ArcGatewayURL (older modules do not). A # malformed or unsupported value logs a warning and the sweep proceeds in normal (non-gateway) mode. if ($arcGatewayUrlToUse -match '^(?i)https://[a-f0-9-]{36}\.gw\.arc\.azure\.(?:com|net)/?$') { $connectivityCommand = Get-Command -Name 'Test-AzureLocalConnectivity' -ErrorAction SilentlyContinue if ($connectivityCommand -and $connectivityCommand.Parameters.ContainsKey('ArcGatewayURL')) { $connectivityParams['ArcGatewayURL'] = [System.Uri]$arcGatewayUrlToUse $arcGatewayModeEnabled = $true Write-InsightLog -Message "Arc Gateway mode enabled: forwarding -ArcGatewayURL '$arcGatewayUrlToUse'. Gateway-supported endpoints are routed via the gateway; only non-gateway endpoints (and the gateway URL) are tested directly." } else { Write-InsightLog -Level 'Warning' -Message "An Arc Gateway URL was resolved ('$arcGatewayUrlToUse') but the installed Test-AzureLocalConnectivity does not expose -ArcGatewayURL; testing the full endpoint set directly instead." } } else { Write-InsightLog -Level 'Warning' -Message "Resolved Arc Gateway URL '$arcGatewayUrlToUse' does not match the expected Arc Gateway form (https://<guid>.gw.arc.azure.com|net); skipping Arc Gateway mode and testing the full endpoint set directly." } } # ///// Main code execution, runs connectivity tests and assigns results to a variable for evaluation by the rules. The results will be a collection of connectivity test results for each endpoint, which will then be evaluated by the associated rules to determine the overall status of the analyzer. # Time the sweep so the completion log records how long it took ON THIS NODE. The upstream # Test-AzureLocalConnectivity emits its own Write-Progress bar during the sweep, but that bar is cleared # when it finishes and leaves no durable record - so we capture the elapsed time here for the transcript/log. $connectivityStopwatch = [System.Diagnostics.Stopwatch]::StartNew() try { # Run the connectivity test splatting with the constructed parameters. -PassThru returns a single # structured PSCustomObject (0.6.8): run-level telemetry / detection state as top-level properties and the # per-endpoint rows under .Results. Bind it directly to a variable. # ErrorAction is already supplied via the $connectivityParams splat above, so it is NOT repeated here # (passing it both in the splat and explicitly throws a "parameter specified more than once" binding error). $ConnectivityResults = Test-AzureLocalConnectivity @connectivityParams } catch { # Unexpected error, log the error and throw a new exception to be caught by the outer try/catch block. This ensures that any unexpected errors during the connectivity test are logged and handled appropriately, allowing for easier troubleshooting and resolution. Write-InsightLog -Level 'Error' -Message "Connectivity test failed with error: $($_.Exception.Message)" throw "Connectivity test failed. Please check network connectivity and firewall settings." } $connectivityStopwatch.Stop() # Check if results were returned and log the count; if not, log a warning. # 0.6.8 -PassThru returns a structured PSCustomObject with the per-endpoint rows nested under .Results and # run-level telemetry / detection state as top-level properties. Extract the rows null-safely (a $null result # or a structured object carrying an empty .Results both collapse to zero rows -> the no-results warning path). $connRows = @() if ($ConnectivityResults -and $ConnectivityResults.PSObject.Properties['Results']) { $connRows = @($ConnectivityResults.Results) } # Log the schema version and warn (non-fatal) if it is below the minimum contract this consumer was written # against. A missing/older/unparseable schema means the producer/consumer contract may not line up - surface # it so a version mismatch is visible in the report rather than manifesting as silently missing fields. if ($ConnectivityResults) { $schemaRaw = if ($ConnectivityResults.PSObject.Properties['SchemaVersion']) { [string]$ConnectivityResults.SchemaVersion } else { '' } if ($schemaRaw) { Write-InsightLog -Message ($localizedData.SchemaVersion -f $schemaRaw) $parsedSchema = $null if (-not [version]::TryParse($schemaRaw, [ref]$parsedSchema) -or $parsedSchema -lt [version]$localizedData.MinimumSchemaVersion) { Write-InsightLog -Level 'Warning' -Message ($localizedData.SchemaVersionBelowMinimum -f $schemaRaw, $localizedData.MinimumSchemaVersion) } } else { Write-InsightLog -Level 'Warning' -Message $localizedData.SchemaVersionMissing } } # ---- Detailed connectivity HTML report link(s) (ReportLinks) -------------------------------------------- # Surface a link to the module's own detailed HTML connectivity report (the full per-endpoint table it renders, # which is not part of the structured rule data) - mirroring the OsConfig analyzer's ReportLinks. The report # engine renders each entry as a RELATIVE anchor ONLY when the file lives inside the InsightReport tree, and # safely skips it otherwise - so a link is never broken. # # IMPORTANT: the module writes its HTML report to BOTH the -ExportPath we passed (inside this node's collected # report folder) AND its own C:\ProgramData\AzStackHci.DiagnosticSettings cache, and it surfaces the ProgramData # copy as .ReportPath. That ProgramData path is OUTSIDE the InsightReport tree, so linking it would be dropped by # the relative-anchor renderer AND never collected to the orchestrator. We therefore PREFER the in-tree # -ExportPath copy (glob) and only fall back to the surfaced .ReportPath when ExportPath has nothing (e.g. an # older module version that does not write to ExportPath). $reportLinks = [System.Collections.Generic.List[object]]::new() $connReportHtml = $null # 1) Prefer THIS node's in-tree ExportPath copy (collectible + renders as a relative anchor). if (Test-Path -LiteralPath $ExportPath) { $connReportHtml = (Get-ChildItem -LiteralPath $ExportPath -Filter "AzureLocal_ConnectivityTest_*_$($env:COMPUTERNAME)_*.html" -File -Recurse -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1).FullName } # 2) Fall back to the module-surfaced ReportPath (may be an absolute ProgramData path or a folder) only when the # in-tree copy was not found. if (-not $connReportHtml -and $ConnectivityResults -and $ConnectivityResults.PSObject.Properties['ReportPath'] -and $ConnectivityResults.ReportPath) { $reportPathRaw = [string]$ConnectivityResults.ReportPath if (Test-Path -LiteralPath $reportPathRaw) { if (Test-Path -LiteralPath $reportPathRaw -PathType Leaf) { $connReportHtml = $reportPathRaw } else { $connReportHtml = (Get-ChildItem -LiteralPath $reportPathRaw -Filter "AzureLocal_ConnectivityTest_*_$($env:COMPUTERNAME)_*.html" -File -Recurse -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1).FullName } } } if ($connReportHtml -and (Test-Path -LiteralPath $connReportHtml)) { $reportLinks.Add([pscustomobject]@{ Title = "View Detailed AzLocal Connectivity Test Report ($env:COMPUTERNAME)"; Path = [string]$connReportHtml }) Write-InsightLog -Message "Linked detailed connectivity HTML report for '$env:COMPUTERNAME': $(Split-Path -Leaf $connReportHtml)." } # NOTE: this block links only THIS node's own detailed connectivity report. Collecting the OTHER cluster # nodes' detailed reports back to the orchestrator is deliberately NOT done here - an analyzer runs per-node # and, in the framework fan-out, the orchestrator's analyzer executes BEFORE the peer nodes have finished, so # any in-analyzer cross-node copy would race ahead and pull missing/stale peer reports. Each node writes its # own report into its collected per-node folder (via -ExportPath above), so every node's report reaches the # orchestrator through the framework's existing post-fan-out folder copy. if ($reportLinks.Count -gt 0) { Add-Member -InputObject $insightAnalyzer -NotePropertyName 'ReportLinks' -NotePropertyValue @($reportLinks.ToArray()) -Force } if($connRows.Count -gt 0) { Write-InsightLog -Message "Connectivity test on node '$env:COMPUTERNAME' completed with $($connRows.Count) result(s) in $([math]::Round($connectivityStopwatch.Elapsed.TotalSeconds, 1))s. Passing results to rules for evaluation." # Capture the run-level telemetry + classification state exposed as top-level properties on the returned # object. The PSObject.Properties guards keep this safe when a field is not surfaced (benign default). $sslInspectionDetected = if ($ConnectivityResults.PSObject.Properties['SSLInspectionDetected']) { [bool]$ConnectivityResults.SSLInspectionDetected } else { $false } $sslInspectedURLs = if ($ConnectivityResults.PSObject.Properties['SSLInspectedURLs']) { @($ConnectivityResults.SSLInspectedURLs) } else { @() } $privateLinkDetected = if ($ConnectivityResults.PSObject.Properties['PrivateLinkDetected']) { [bool]$ConnectivityResults.PrivateLinkDetected } else { $false } $privateLinkCriticalArray = if ($ConnectivityResults.PSObject.Properties['PrivateLinkCriticalArray']) { @($ConnectivityResults.PrivateLinkCriticalArray) } else { @() } $privateLinkProxyBypassArray = if ($ConnectivityResults.PSObject.Properties['PrivateLinkProxyBypassArray']) { @($ConnectivityResults.PrivateLinkProxyBypassArray) } else { @() } # v0.6.8 VERIFIED proxy-bypass state: BypassMissing = non-critical RFC1918 endpoints (Private Endpoint # in use) that are NOT on the machine's proxy bypass list; BypassConfirmed = those correctly excluded. $privateLinkBypassMissingArray = if ($ConnectivityResults.PSObject.Properties['PrivateLinkBypassMissingArray']) { @($ConnectivityResults.PrivateLinkBypassMissingArray) } else { @() } $privateLinkBypassConfirmedArray = if ($ConnectivityResults.PSObject.Properties['PrivateLinkBypassConfirmedArray']) { @($ConnectivityResults.PrivateLinkBypassConfirmedArray) } else { @() } # Detected proxy configuration exposed as top-level properties (0.6.8): ProxyEnabled (bool), ProxyServer # (raw netsh winhttp string), ProxyHttp / ProxyHttps (per-scheme proxies, blank when unset). $proxyEnabled = if ($ConnectivityResults.PSObject.Properties['ProxyEnabled']) { [bool]$ConnectivityResults.ProxyEnabled } else { $false } $proxyServer = if ($ConnectivityResults.PSObject.Properties['ProxyServer']) { [string]$ConnectivityResults.ProxyServer } else { '' } $proxyHttp = if ($ConnectivityResults.PSObject.Properties['ProxyHttp']) { [string]$ConnectivityResults.ProxyHttp } else { '' } $proxyHttps = if ($ConnectivityResults.PSObject.Properties['ProxyHttps']) { [string]$ConnectivityResults.ProxyHttps } else { '' } # Certificate revocation (CRL / OCSP) reachability state (0.6.8 top-level properties). When a leaf chain # is trusted but its revocation responder is offline, the module sets CRLOfflineDetected and lists the # affected PARENT endpoint URLs in CRLOfflineURLs. $crlOfflineDetected = if ($ConnectivityResults.PSObject.Properties['CRLOfflineDetected']) { [bool]$ConnectivityResults.CRLOfflineDetected } else { $false } $crlOfflineURLs = if ($ConnectivityResults.PSObject.Properties['CRLOfflineURLs']) { @($ConnectivityResults.CRLOfflineURLs) } else { @() } # Correlate the run-level SSL-inspection signal with the per-endpoint certificate detail. v0.6.7 stamps # each intercepted endpoint with ResultCategory='SSLInspected' AND the observed (re-signed) certificate # chain - the leaf (CertificateIssuer / CertificateSubject / CertificateThumbprint) plus the intermediate # and root cert fields. We project all three tiers into a compact per-endpoint detail list so the # SslInspection rule can name the intercepting CA (and the chain it presented) against each endpoint, # instead of emitting only a bare URL list. This reads ROW members (not the container NoteProperties), so # it is safe to build via the pipeline here. The PSObject.Properties guards let a row without the # chain columns degrade gracefully to URL-only / leaf-only detail (empty strings). $sslInspectedEndpoints = @( $connRows | Where-Object { $_.PSObject.Properties['ResultCategory'] -and ([string]$_.ResultCategory -ieq 'SSLInspected') } | ForEach-Object { [PSCustomObject]@{ URL = [string]$_.URL MachineName = if ($_.PSObject.Properties['MachineName']) { [string]$_.MachineName } else { '' } CertificateIssuer = if ($_.PSObject.Properties['CertificateIssuer']) { [string]$_.CertificateIssuer } else { '' } CertificateSubject = if ($_.PSObject.Properties['CertificateSubject']) { [string]$_.CertificateSubject } else { '' } CertificateThumbprint = if ($_.PSObject.Properties['CertificateThumbprint']) { [string]$_.CertificateThumbprint } else { '' } IntermediateCertificateIssuer = if ($_.PSObject.Properties['IntermediateCertificateIssuer']) { [string]$_.IntermediateCertificateIssuer } else { '' } IntermediateCertificateSubject = if ($_.PSObject.Properties['IntermediateCertificateSubject']) { [string]$_.IntermediateCertificateSubject } else { '' } IntermediateCertificateThumbprint = if ($_.PSObject.Properties['IntermediateCertificateThumbprint']) { [string]$_.IntermediateCertificateThumbprint } else { '' } RootCertificateIssuer = if ($_.PSObject.Properties['RootCertificateIssuer']) { [string]$_.RootCertificateIssuer } else { '' } RootCertificateSubject = if ($_.PSObject.Properties['RootCertificateSubject']) { [string]$_.RootCertificateSubject } else { '' } RootCertificateThumbprint = if ($_.PSObject.Properties['RootCertificateThumbprint']) { [string]$_.RootCertificateThumbprint } else { '' } } } ) # Correlate the revocation-offline signal with the CRL / OCSP dependency endpoints the module appends as # their own rows. Each dependency row carries Source='CRL/OCSP dependency for <parent>' and a Note that # cross-references the parent endpoint. We project them (Kind, URL, Port, ParentSource, Note, Layer7Status, # Layer7Response, IPAddress) so the CrlRevocationOffline rule can name each revocation responder against its # parent and call out the ones whose own port-80 test was BLOCKED (the concrete egress gaps to open). This # reads ROW members, so building it via the pipeline here is safe. $revocationDependencyEndpoints = @( $connRows | Where-Object { $_.PSObject.Properties['Source'] -and ([string]$_.Source -imatch '^(CRL|OCSP) dependency for ') } | ForEach-Object { # Derive the responder kind from the Source prefix the module stamps on the dependency row. $dependencyKind = if ([string]$_.Source -imatch '^OCSP') { 'OCSP' } else { 'CRL' } [PSCustomObject]@{ Kind = $dependencyKind URL = [string]$_.URL Port = if ($_.PSObject.Properties['Port']) { $_.Port } else { 80 } ParentSource = [string]$_.Source Note = if ($_.PSObject.Properties['Note']) { [string]$_.Note } else { '' } Layer7Status = if ($_.PSObject.Properties['Layer7Status']) { [string]$_.Layer7Status } else { '' } Layer7Response = if ($_.PSObject.Properties['Layer7Response']) { [string]$_.Layer7Response } else { '' } IPAddress = if ($_.PSObject.Properties['IPAddress']) { [string]$_.IPAddress } else { '' } } } ) # Build a structured property report so the run-level telemetry surfaces in the analyzer/HTML output. # This lets a reviewer see which request method / parallelism was used, the perceived (wall-clock) vs # summed Layer-7 time, the measured download speed, and the node the sweep ran on (MachineName). # The SSLInspected / PrivateLink counts give an at-a-glance summary of the two specialised signals # (the per-URL detail lives on the dedicated SslInspection / PrivateLinkConfiguration rules below). # # The telemetry is rendered as a tall Section | Metric | Value table (one metric per row) rather than a # single very wide row. As more signals are added (proxy, Arc, detection counts) a one-row-wide table # forces a large horizontal scroll in the HTML report; pivoting to a vertical key/value layout keeps the # table narrow and groups related metrics under a Section label. This stays within the single property # report contract (Section/Metric/Value rows) so no framework / renderer change is required. # Resolve each value once (with the same fallbacks as before) so the row build below stays readable. $requestMethodValue = if ($ConnectivityResults.PSObject.Properties['RequestMethod']) { $ConnectivityResults.RequestMethod } else { $RequestMethod } $parallelismValue = if ($ConnectivityResults.PSObject.Properties['Parallelism']) { $ConnectivityResults.Parallelism } else { $Parallelism } $downloadSpeedValue = if ($ConnectivityResults.PSObject.Properties['DownloadSpeed']) { $ConnectivityResults.DownloadSpeed } else { 'N/A' } $totalDurationSecondsValue = if ($ConnectivityResults.PSObject.Properties['TotalDurationSeconds']) { $ConnectivityResults.TotalDurationSeconds } else { 0 } $layer7WallClockSecondsValue = if ($ConnectivityResults.PSObject.Properties['Layer7WallClockSeconds']) { $ConnectivityResults.Layer7WallClockSeconds } else { 0 } $layer7TotalDurationSecondsValue = if ($ConnectivityResults.PSObject.Properties['Layer7TotalDurationSeconds']) { $ConnectivityResults.Layer7TotalDurationSeconds } else { 0 } $layer7TestedEndpointsValue = if ($ConnectivityResults.PSObject.Properties['Layer7TestedEndpoints']) { $ConnectivityResults.Layer7TestedEndpoints } else { 0 } # Ordered list of (Section, Metric, Value) tuples. Order here is the order the rows render in the table. # Metric labels are spaced, human-readable names. "Elapsed Time" is the real wall-clock time the Layer-7 # sweep took end-to-end; "Summed Duration" is the sum of every endpoint's individual time (higher than the # elapsed time when endpoints run in parallel) - the two are surfaced separately so a reviewer can see the # parallelism benefit at a glance. # NOTE: each row MUST be comma-separated. Without the trailing comma the newline-separated @(...) sub-arrays # are each written to the pipeline and ENUMERATED by the outer @(), flattening the rows of 3 values into one # flat array of scalars. The foreach below would then index into a single string ($row[0] -> first CHAR), # rendering one character per table cell. Commas build a genuine array-of-arrays and preserve each row. $telemetryRows = @( @('Run Config', 'Machine Name', $env:COMPUTERNAME), @('Run Config', 'Azure Region', $AzureRegion), @('Run Config', 'Request Method', $requestMethodValue), @('Run Config', 'Test Parallelism', $parallelismValue), @('Node / Arc', 'Cluster Registration Status', $ConnectionStatus), @('Node / Arc', 'Arc Agent Status', $arcAgentStatus), @('Node / Arc', 'Arc Connection Type', $(if ($arcConnectionType) { $arcConnectionType } else { 'N/A' })), @('Node / Arc', 'Arc Gateway Mode', $arcGatewayModeEnabled), @('Node / Arc', 'Arc Gateway URL', $(if ($arcGatewayUrlToUse) { $arcGatewayUrlToUse } else { 'N/A' })), @('Proxy', 'Proxy Enabled', $proxyEnabled), @('Proxy', 'Proxy Server', $(if ($proxyServer) { $proxyServer } else { 'N/A' })), @('Proxy', 'Proxy (HTTP)', $(if ($proxyHttp) { $proxyHttp } else { 'N/A' })), @('Proxy', 'Proxy (HTTPS)', $(if ($proxyHttps) { $proxyHttps } else { 'N/A' })), @('Timing', 'Download Speed', $downloadSpeedValue), @('Timing', 'Total Duration (seconds)', $totalDurationSecondsValue), @('Timing', 'Layer 7 Elapsed Time (seconds)', $layer7WallClockSecondsValue), @('Timing', 'Layer 7 Summed Duration (seconds)', $layer7TotalDurationSecondsValue), @('Timing', 'Layer 7 Tested Endpoints', $layer7TestedEndpointsValue), @('Timing', 'Total Endpoints', $connRows.Count), @('Detection', 'SSL Inspection Detected', $sslInspectionDetected), @('Detection', 'SSL Inspected Count', $sslInspectedURLs.Count), @('Detection', 'Private Link Detected', $privateLinkDetected), @('Detection', 'Private Link Critical Count', $privateLinkCriticalArray.Count), @('Detection', 'Private Link Bypass Missing Count', $privateLinkBypassMissingArray.Count), @('Detection', 'Private Link Bypass Confirmed Count', $privateLinkBypassConfirmedArray.Count), @('Detection', 'Private Link Proxy Bypass Count', $privateLinkProxyBypassArray.Count), @('Detection', 'CRL Offline Detected', $crlOfflineDetected), @('Detection', 'CRL Offline Count', $crlOfflineURLs.Count) ) $reportObject = Initialize-InsightAnalyzerPropertyReport -Name 'Connectivity Run Telemetry' foreach ($telemetryRow in $telemetryRows) { $reportObject.Data += [PSCustomObject]@{ Section = $telemetryRow[0] Metric = $telemetryRow[1] Value = [string]$telemetryRow[2] } } $insightAnalyzer.Properties = $reportObject # Pass results to rules for evaluation. Each rule evaluates the connectivity results and sets its # status accordingly, which then rolls up to the analyzer status at the end. # 1) Core endpoint connectivity rule - buckets rows by the v0.6.7 ResultCategory field # (ConnectivityFailure -> FAILURE, ConfigGap (missing KeyVault endpoint parameter) -> SUCCESS (instead of WARNING); # SSLInspected/PrivateLink are handled by the dedicated rules below so they are not double-counted as hard connectivity failures). $insightAnalyzer.Rules += @( & $PSScriptRoot\..\rules\AzStackHci.DiagnosticSettings.NetworkConnectivity.ps1 -ConnectivityResults $ConnectivityResults ) # 2) SSL-inspection rule - warns when a TLS-intercepting proxy was detected in front of Azure Local # endpoints (breaks certificate pinning / Arc onboarding). Driven by the run-level state captured above, # enriched with the per-endpoint certificate detail so the rule can name the intercepting CA. $insightAnalyzer.Rules += @( & $PSScriptRoot\..\rules\AzStackHci.DiagnosticSettings.SslInspection.ps1 ` -SSLInspectionDetected $sslInspectionDetected ` -SSLInspectedURLs $sslInspectedURLs ` -SSLInspectedEndpoints $sslInspectedEndpoints ) # 3) Private Link configuration rule - FAILS on critical Arc endpoints that resolved to an RFC1918 # address (unsupported), and WARNS on non-critical endpoints that resolve to an RFC1918 address # (Private Endpoint in use) but are missing from the proxy bypass list (v0.6.8 verified signal; # falls back to the legacy blanket advisory when verification did not run). $insightAnalyzer.Rules += @( & $PSScriptRoot\..\rules\AzStackHci.DiagnosticSettings.PrivateLinkConfiguration.ps1 ` -PrivateLinkDetected $privateLinkDetected ` -PrivateLinkCriticalArray $privateLinkCriticalArray ` -PrivateLinkProxyBypassArray $privateLinkProxyBypassArray ` -PrivateLinkBypassMissingArray $privateLinkBypassMissingArray ` -PrivateLinkBypassConfirmedArray $privateLinkBypassConfirmedArray ) # 4) Certificate revocation (CRL/OCSP) reachability rule - WARNS when an endpoint's certificate chain is # trusted but its revocation check (CRL / OCSP) is offline because the CA's responder is unreachable. # This is explicitly NOT SSL inspection; it is surfaced separately so it is not mistaken for a hard # connectivity failure. Driven by the run-level state captured above plus the per-dependency detail. $insightAnalyzer.Rules += @( & $PSScriptRoot\..\rules\AzStackHci.DiagnosticSettings.CrlRevocationOffline.ps1 ` -CRLOfflineDetected $crlOfflineDetected ` -CRLOfflineURLs $crlOfflineURLs ` -RevocationDependencyEndpoints $revocationDependencyEndpoints ) # 5) Azure connection status rule - surfaces this node's Azure Local cloud registration status # (Get-AzureStackHCI ConnectionStatus) and Arc for Servers agent status (azcmagent) as a single # per-node output. Both values were gathered locally above. The Insights framework runs this analyzer # on every cluster node, so each node reports its own pair and the aggregate cluster picture is the set # of per-node rule outputs. $insightAnalyzer.Rules += @( & $PSScriptRoot\..\rules\AzStackHci.DiagnosticSettings.AzureConnectionStatus.ps1 ` -NodeName $env:COMPUTERNAME ` -CloudRegistrationStatus ([string]$ConnectionStatus) ` -ArcAgentStatus ([string]$arcAgentStatus) ` -PrivateLinkScopeEnabled ([string]$privateLinkScopeEnabled) ` -ArcAgentDetails $arcAgentDetails ) } else { # No results returned from the connectivity test, log a warning and set the analyzer status to WARNING. Write-InsightLog -Level 'Warning' -Message "Connectivity test returned no results. This may indicate a problem with the test execution or connectivity." $insightAnalyzer.Status = [InsightStatus]::WARNING } } catch { # gracefully handle not supported scenarios such as when the environment does not meet the requirements if ($_.Exception -is [System.NotSupportedException]) { Write-InsightLog -Level 'Informational' -Message $_.Exception.Message $insightAnalyzer.Status = [InsightStatus]::NOT_APPLICABLE } else { $_ | Write-InsightLog -Level 'Exception' $insightAnalyzer.ScriptStackTrace = Get-FormattedException -Exception $_.Exception $insightAnalyzer.Status = [InsightStatus]::UNKNOWN } } <# END ANALYZER LOGIC #> # enumerate the rules and set the analyzer status based on the rules status # we assume that if any rule is in failure, the analyzer is in failure # if any rule is in warning, the analyzer is in warning # if all rules are in success of info, the analyzer is in success if ($insightAnalyzer.Rules.Status -icontains [InsightStatus]::FAILURE) { $insightAnalyzer.Status = [InsightStatus]::FAILURE } elseif ($insightAnalyzer.Rules.Status -icontains [InsightStatus]::WARNING) { $insightAnalyzer.Status = [InsightStatus]::WARNING } $insightAnalyzer.Duration = New-TimeSpan -Start $insightAnalyzer.OccurrenceTimeUTC -End $([System.DateTime]::UtcNow) # Cache the analyzer results, write event and return result Set-InsightCache -Type 'Analyzer' -Data $insightAnalyzer Write-InsightEvent -Insight $insightAnalyzer return $insightAnalyzer # SIG # Begin signature block # MIInRgYJKoZIhvcNAQcCoIInNzCCJzMCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDwpLFao2ICa1yX # 9MCixYgjrEUbe5fG04A33vPMf3OL9aCCDLowggX1MIID3aADAgECAhMzAAACHU0Z # yE7XD1dIAAAAAAIdMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD # b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQzWhcNMjcwNDE1MTg1 # OTQzWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD # VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB # DwAwggEKAoIBAQDQvewXxx9gZZFC6Ys1WBay8BJ8kGA4JQnH5CMafqOASlTpK9H8 # o5ZXTXt0caVQTNMUPt445wXYD+dFtaKWTwDn1I52oUSrC9vJin1Gsqt+zyKJL5Dg # 3eQXbQNR61DmMy20GLTIO3SFed9Rfi/ophgCLGFLDR3r0KvHjwMb/jYWS0celV/4 # Lz27LfAekm8v9E5IXaeiXbAUYZKK090n4CVl3JBtbN+9DtI9SNu/yjvozW52/u7R # X/Ttpa/KDlpuokZ+Zcbvmtd9ur9gFLvZzh41o9MsE/clQtdaFWGvuo6Jua/ntpgk # ey3E5/vBFe+MJPG6phdnuo6r57ZudCudiI1bAgMBAAGjggGbMIIBlzAOBgNVHQ8B # Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O # BBYEFH6QuMwqcPG0hQlQ6c5jCtTTLrVeMEUGA1UdEQQ+MDykOjA4MR4wHAYDVQQL # ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xFjAUBgNVBAUTDTIzMDAxMis1MDc1NTkw # HwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEwYAYDVR0fBFkwVzBVoFOg # UYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0 # JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNybDBtBggrBgEFBQcBAQRh # MF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMv # Y2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy # dDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4ICAQBKTbYOjzwTG/DXGaz9 # s6+fQeaTtDcFmMY+5UyVFCyj7Pv+5i37qfX8lSL/tBIfYQfWsMuBQlfZurJD6r4H # VJ2CeH+1fgiq8dcHdVKoZ3Sa2qXoX3cq9iS8cVb06B7+5/XJ7I0OxHH9fDsvJ3T3 # w5V/ZtAIFmLrl+P0CtG+92uzRsn0nTbdFjOkLMLWPLAU3THohKRlSEMgFJpPkm5n # 5UAZ35xX6FWCrDLsSKb555bTifwa8mJBwdlof0bmfYidH+dxZ1FdDxvLnNl9zeKs # A4kejaaIqqIPguhwAti5Ql7BlTNoJNwxCvBmqW2MQLnCkYN/VVUsR3V2x/rcTNzo # Bf/Z/SpROvdaA2ZOOd1uioXJt3tdLQ7vHpqpib0KfWr/FWXW10q38VxfCnRQBqzb # SuztR7nEMuzX7Ck+B/XaPDXd1qh72+QYyB0Z2VzWmO9zsnb9Uq/dwu8LGeQqnyu6 # 7SDGACvnXii2fb9+US492VTnXSnFKyqwgzUyFMtZK1/sHYTv6bG4TtQUygQxTN+Z # V+aJIlKO2MqZ7bKrAnOzS9m6NgoTdWOq11bTOZwKlIEV/EhV9SWkDmdpR/hPPT2v # 6TEj4F8PT/zHjRezIU5c/DGlt/VhY/pK0XkJtEyMmmS1BMtjU/rqBZVMIm3dnxQs # /TBByr+Cf8Z1r7aifQVQ+WSqzjCCBr0wggSloAMCAQICEzMAAAA5O7Y3Gb8GHWcA # AAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX # YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg # Q29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRl # IEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoXDTM2MDMyMjIyMTMwNFow # VzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEo # MCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAyNDCCAiIwDQYJ # KoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeqlRYHNa265v4IY9fH8TKh # emHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo0dtS/EW6I/yEL/bLSY8h # KpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATvQVL4tcf03aTycsz8QeCd # M0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a1uv1zerOYMnsneRRwCbp # yW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1FyQfK0fVkaya8SmVHQ/t # Of23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfOGSWHIIV4YrTJTT6PNty5 # REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7ttOu1bVnXfHaqPYl2rPs # 20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJuz2MXMCt7iw7lFPG9LXK # Gjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxSCwyoGIq0PhaA7Y+VPct5 # pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOmVQop36wUVUYklUy++vDW # eEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3SkE/xIkgpfl22MM1itkZ # 35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8EBAMCAYYwEAYJKwYBBAGC # NxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPXLQaUEggxMBkGCSsGAQQB # gjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU # ci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2Ny # bC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0MjAx # MV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAwTgYIKwYBBQUHMAKGQmh0 # dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0MjAx # MV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOCAgEAFJQfOChP7onn6fLI # MKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D5W4wMwYeLystcEqfkjz4 # NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBYnbu0+THSuVHTe0VTTPVh # ily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSIvgn0JksVBVMYVI5QFu/q # hnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6aR9y34aiM1qmxaxBi6OU # nyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4wPKC5OmHm1DQIt/MNokbb # H3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7RTX8AdBPo0I6OEojf39z # uFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK/fg8B2qjW88MT/WF5V5u # vZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSKYBv0VisCzfxgeU+dquXW # 9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkwYTu/9dLeH2pDqeJZAABV # DWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVTQl0v4q8J/AUmQN5W4n10 # 1cY2L4A7GTQG1h32HHAvfQESWP0xghniMIIZ3gIBATBuMFcxCzAJBgNVBAYTAlVT # MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jv # c29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIdTRnITtcPV0gAAAAAAh0w # DQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYK # KwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIExKMW5J # B8jT+nfBL/NmNaKqLdsskoHVARP+1khBacemMEIGCisGAQQBgjcCAQwxNDAyoBSA # EgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20w # DQYJKoZIhvcNAQEBBQAEggEABt5ni37WympCszeEiuPy9em8Tv6FEm8dwtTJ548E # 17Hw6WXqSsQP1QutVC1AERX8rp3T4/ax+66TRnTPa6hvn8rAtjtIO0DHIbtM6M1E # rbDHTTQwuetR6ackhhxK2RyeAGYZg9tf+1x308X3vw1SXJaQAMYrDhLiKB6MLrbq # U1YEUKci5zVI4Hte1v0y4RhkPXx6EojJ6JkPHexP4BVIQcKUStorK46w1/3lqcCL # 135EC+BKPCAg2ELl7jUIwUV6UdlybhtF7Ypzv24q1SqF/PdarxjLJMswVpzTk2Sm # pvj5G+FC9253LvX2+FyPk23A5zl3Ra+OE5fMcgOG/izlYaGCF5QwgheQBgorBgEE # AYI3AwMBMYIXgDCCF3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUD # BAIBBQAwggFSBgsqhkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoD # ATAxMA0GCWCGSAFlAwQCAQUABCBea/ab90uTJx8L4DeXlT5vxWzw1mo38n4weF1B # r9+kagIGajFoxA+XGBMyMDI2MDcxNjIyNTMwMi40MzZaMASAAgH0oIHRpIHOMIHL # MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk # bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxN # aWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRT # UyBFU046REMwMC0wNUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0 # YW1wIFNlcnZpY2WgghHqMIIHIDCCBQigAwIBAgITMwAAAiQ7hCGwLKxkIgABAAAC # JDANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGlu # Z3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBv # cmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAe # Fw0yNjAyMTkxOTM5NTlaFw0yNzA1MTcxOTM5NTlaMIHLMQswCQYDVQQGEwJVUzET # MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV # TWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmlj # YSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046REMwMC0wNUUw # LUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIi # MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCj6W3UaQ2Zr4hNvSy7j7UMPFVy # s7aExGB+JFwykzzXg3jayYm9gOLXJ7tNhU2emhrLQCOZcgLvz6FkqmghzQxzmkgK # tLYiKaEzhogO/ce0lThdLNdVtMwQOYgo+XtXAZcViBX4LcHk38RusZiF7wxSa5t/ # Lxic04+Z/hly1gJQpIeFDqp4a9PuLt8rsfH05vW9pU9uriGdDxfJXn/lc49CxbXq # A3EX17L24bc6t+mFuPDAJKKpai3XXqF2nJlpTPfdrA29sWTSNKig9CtBC5tzQj0f # lbsa/4wqO9u+RkuwpZb3b7qnW5FdFrDR1vQmXfjlyUP9ZO38839NwSuiHtvsFCNk # TNIX8OL5XVq1nsKyu//GeIZ9YuxsfLBedqG024PDERyrAs0pvfUWOLapVQajHPoC # nuNSKvbEh7s5IQ0YgupGji+H7rIDx2/mIEI+6Q8WwBtk3Yxyhjj0GXw909i0EkTk # Vyy+1yADjwSC8bw2qM4+Mc4hyytlZzSc0IPUBq1YGnYwCjIwa5/lMW0pFn/HpJdB # 6XeMuTtYTOpaPoo64FjQryLXWjd4ovpw5lOw7X+v3E9kwN9VBC+wJESBECC1gZMC # S5TaVwfE1w4pnXXb1qT9bjgRsPg4dklruUTdon/3SNt0a0Q5Nc2Ul+rMlQxXoP9i # sXwMNnKO5JJkqRDRVQIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFHMfkX1u/zJLCMe0 # gqYitx1tAHeoMB8GA1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1Ud # HwRYMFYwVKBSoFCGTmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3Js # L01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggr # BgEFBQcBAQRgMF4wXAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNv # bS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIw # MTAoMSkuY3J0MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgw # DgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3DQEBCwUAA4ICAQA+wHSbmhIpM8CRVZ4t # k624hQ+LdZXE4qoeQui77CeNa3jq1FOzi7MRKkko6diEDHXPNWvAagxastCewPzm # 5TCNh1s4qCHh4R2G/r48wU/Mpc68/WDmJy5CIQn/Fwps1sbNUEu7Bzg004qULIVJ # 963jo/am4xwKgwh+vSVL7/dhsfT7dvhpRddbYLQTHZgwuNB6QhcEEsgogLVwNRj3 # 7VEWZDiwoMdxyC7YYrQu6MCVtizHnOtkSX7FqIoi6jlcfqfo619uDH9r8k2qAOHC # eEAqKXKymIXDMcGGlEdDFbYiDZgPCBM0IHgAeilUSon07wjHu0e0ssBmtBafPb4G # d+5FuRnWG3XGe91NCpLKqmFa/4GkVz9OMzZUg8oczxC/4JT3Hf45JEtszToXwNsk # V3JNCcu2IItr6SJHmi3EDVADDRSNhdzFRpYmplGElPl5GRoPtJiDEvRIbv5MFKIw # 2x9gnehf5IvBjC4ZkBg+4GTpqGE3mmnzF3nIekOkX4ug0/0mN2CSarhuSi9NmHIO # pUN2eQHUtgTb/+Gmq7gktCMwIq/JOCYIiTYqpv1objAGKdWMPCrlSyNAs0jZYzkh # a535158NMx+wBGvsfFoVsCMG5Ocp6vW6CXyuWRbUVqMU1OrQbHfdyzJpbhJC1PbA # ZIyJCbN+VBgDTAzTKY8w4ISSwTCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkA # AAAAABUwDQYJKoZIhvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpX # YXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQg # Q29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRl # IEF1dGhvcml0eSAyMDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVow # fDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1Jl # ZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMd # TWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUA # A4ICDwAwggIKAoICAQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX # 9gF/bErg4r25PhdgM/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1q # UoNEt6aORmsHFPPFdvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8d # q6z2Nr41JmTamDu6GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byN # pOORj7I5LFGc6XBpDco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2k # rnopN6zL64NF50ZuyjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4d # Pf0gz3N9QZpGdc3EXzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgS # Uei/BQOj0XOmTTd0lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8 # QmguEOqEUUbi0b1qGFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6Cm # gyFdXzB0kZSU2LlQ+QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzF # ER1y7435UsSFF5PAPBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQID # AQABo4IB3TCCAdkwEgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQU # KqdS/mTEmr6CkTxGNSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1 # GelyMFwGA1UdIARVMFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0 # dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0 # bTATBgNVHSUEDDAKBggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMA # QTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbL # j+iiXGJo0T2UkFvXzpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1p # Y3Jvc29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0w # Ni0yMy5jcmwwWgYIKwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3 # Lm1pY3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIz # LmNydDANBgkqhkiG9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwU # tj5OR2R4sQaTlz0xM7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN # 3Zi6th542DYunKmCVgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU # 5HhTdSRXud2f8449xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5 # KYnDvBewVIVCs/wMnosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGy # qVvfSaN0DLzskYDSPeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB6 # 2FD+CljdQDzHVG2dY3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltE # AY5aGZFrDZ+kKNxnGSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFp # AUR+fKFhbHP+CrvsQWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcd # FYmNcP7ntdAoGokLjzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRb # atGePu1+oDEzfbzL6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQd # VTNYs6FwZvKhggNNMIICNQIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzAR # BgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1p # Y3Jvc29mdCBDb3Jwb3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2Eg # T3BlcmF0aW9uczEnMCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOkRDMDAtMDVFMC1E # OTQ3MSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEw # BwYFKw4DAhoDFQCmCPHbmseASfe//bGtX9eQG+0+46CBgzCBgKR+MHwxCzAJBgNV # BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w # HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29m # dCBUaW1lLVN0YW1wIFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA7gNv/DAiGA8y # MDI2MDcxNjE0NTgwNFoYDzIwMjYwNzE3MTQ1ODA0WjB0MDoGCisGAQQBhFkKBAEx # LDAqMAoCBQDuA2/8AgEAMAcCAQACAiC8MAcCAQACAhISMAoCBQDuBMF8AgEAMDYG # CisGAQQBhFkKBAIxKDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEA # AgMBhqAwDQYJKoZIhvcNAQELBQADggEBAEqOLYjrH9QuuCb4Svkg7zP+lNl+ttHP # 88sVXmEGkq68tsjsoN0ziG3pgOdGtVm8VCpVxq+7URtVHfm0vyvJgzfT5iUVIxQs # 9cZKmDvEbjdwQmsg/xrVJEKoA7/cizLTHfUbRCkmBn/Em63K7IsXvThDPCF3wX4K # +I6TnbWaMeAs5GKVUbQi+08svfZmhRum+yKO0h3fe920pIeaq3S1Aw+R9PnRKORD # w+ElXQEBffNDZGgjG4tr41afY90t3dxAKF/GofdCzXkiwALftpicEhltpq7O2+ol # dW5juPOVcRKExVju23kY+UZ2egSx/M7zAJM0arI/raxGGQAnXF6J8hkxggQNMIIE # CQIBATCBkzB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G # A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYw # JAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAiQ7hCGw # LKxkIgABAAACJDANBglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqG # SIb3DQEJEAEEMC8GCSqGSIb3DQEJBDEiBCCzRC8clHJgnKmvkZmIFwe7iWB2bE/B # wOmSTCEWBmYhpTCB+gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIEghPTdqm/dR # yZ0BczXcdloVEqICdcmpVNbH9CEVzWSOMIGYMIGApH4wfDELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT # FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUt # U3RhbXAgUENBIDIwMTACEzMAAAIkO4QhsCysZCIAAQAAAiQwIgQgcQwwdMq7LoNC # O10ouGy2LWnLvs38HFLlNPO0dgd9MLkwDQYJKoZIhvcNAQELBQAEggIAMIDxwr2R # y8fTpDMk6Y79+F1nXqewHJvDVxKjtO9eN8G+FkvcdwO6iQ24dezYhewixtWDVZT4 # UEX3cJpG1qxP6MbSXeRS0d96IlwU62KXjqtcp4CbSxtFee0tnKkeLC0Hx01xswWe # A5xmTd6lmXSwGQgy1KspM9fOgO/uzDXUIARg/J7KVuJcXu9gP+SAhqp756tKPvrf # OEbW7A4qLAgXTLFA5SVvLXAhCtyi5hTWKB/NLf4TBGegG+slKLuYJp7pCdtXbo4T # YlDm9TcF6m2K/6j2PbhEnwTJIsAmnjTlTnoURpw2wL1kfXRadD5sgoXciQ61lk9J # kWvIrqC1O9u6a3kMLqFzsLKSmw/n5QUyfIdnnkt3btpXXTVuNpjTsRJ4cUXSC3kR # tHhe5eGk2DtmVsNOxI3lbvvwDfLAAmNrVWs2ZGxGiMzqZbW//H62BgXDLq+9y6XR # Sz41TvK3PB29az0mzfDgZbPC4r4V/lp7YleckzaXqWvAWCjy7QMPDFCnYw13SX5t # F5TupNkV6TpwJtMjG1fo2BfiyRmuB7eN5IOMkrQdKZVKDPyYOrduWdvt3s9+MXmR # 0Cu5jSufRHKlC8NYunsoW6nBn0jzNXngJQDkAhZEaNqMLipPokyKzwBZqEg1wTJw # dAUhQOtibyWope3kgxsQ4XoEPS2Ttz3Gvms= # SIG # End signature block |