1125 lines
48 KiB
PowerShell
1125 lines
48 KiB
PowerShell
param(
|
|
[string]$ApiBaseUrl = "http://192.168.200.61:18121/api/v1",
|
|
[string]$ClusterID = "cfc0743d-d960-49fb-9de8-96e063d5e4aa",
|
|
[string]$ActorUserID = "f67d943f-5397-4b3a-a229-695fe67ad700",
|
|
[string]$PrimaryEntryNodeName = "test-1",
|
|
[string]$AlternateEntryNodeName = "test-3",
|
|
[string]$ExitNodeName = "test-2",
|
|
[string]$DockerSSH = "test-docker",
|
|
[int]$PreRebuildBatchCount = 18,
|
|
[int]$PostRebuildBatchCount = 36,
|
|
[int]$PacketsPerBatch = 8,
|
|
[int]$BatchDelayMilliseconds = 25,
|
|
[string]$RequiredNodeVersion = "0.2.185",
|
|
[string]$ResultPath = "artifacts\c18z11-live-service-channel-entry-pool-smoke-result.json"
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = "Stop"
|
|
Add-Type -AssemblyName System.Net.Http
|
|
|
|
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).ProviderPath
|
|
$runId = "c18z11-" + (Get-Date -Format "yyyyMMdd-HHmmss")
|
|
$resourceId = "vpn-$runId"
|
|
|
|
function Invoke-Api {
|
|
param(
|
|
[string]$Method,
|
|
[string]$Path,
|
|
[object]$Body = $null
|
|
)
|
|
$uri = "$ApiBaseUrl$Path"
|
|
try {
|
|
if ($null -eq $Body) {
|
|
return Invoke-RestMethod -Method $Method -Uri $uri -TimeoutSec 30
|
|
}
|
|
return Invoke-RestMethod -Method $Method -Uri $uri -ContentType "application/json" -Body ($Body | ConvertTo-Json -Depth 80) -TimeoutSec 30
|
|
}
|
|
catch {
|
|
$statusCode = $null
|
|
if ($_.Exception.Response) {
|
|
$statusCode = [int]$_.Exception.Response.StatusCode
|
|
}
|
|
$details = $_.ErrorDetails.Message
|
|
if (-not $details) {
|
|
$details = $_.Exception.Message
|
|
}
|
|
throw "$Method $Path failed with HTTP $statusCode`: $details"
|
|
}
|
|
}
|
|
|
|
function Get-NodeByName {
|
|
param([string]$Name)
|
|
$nodes = (Invoke-Api -Method GET -Path "/clusters/$ClusterID/nodes?actor_user_id=$ActorUserID").nodes
|
|
$node = @($nodes | Where-Object { $_.name -eq $Name }) | Select-Object -First 1
|
|
if ($null -eq $node) {
|
|
throw "Node '$Name' was not found in cluster $ClusterID"
|
|
}
|
|
return $node
|
|
}
|
|
|
|
function Get-MeshPort {
|
|
param([string]$Name)
|
|
switch ($Name) {
|
|
"test-1" { return 19131 }
|
|
"test-2" { return 19132 }
|
|
"test-3" { return 19133 }
|
|
default { return 19131 }
|
|
}
|
|
}
|
|
|
|
function Get-EntryBaseUrlForNode {
|
|
param([object]$Node)
|
|
return "http://192.168.200.61:$(Get-MeshPort -Name $Node.name)"
|
|
}
|
|
|
|
function Enable-TestMeshListener {
|
|
param([object]$Node)
|
|
$port = Get-MeshPort -Name $Node.name
|
|
Invoke-Api -Method PUT -Path "/clusters/$ClusterID/nodes/$($Node.id)/workloads/mesh-listener/desired" -Body @{
|
|
actor_user_id = $ActorUserID
|
|
desired_state = "enabled"
|
|
runtime_mode = "container"
|
|
version = "c18z11-live-fsc-entry-pool"
|
|
config = @{
|
|
listen_addr = "0.0.0.0:$port"
|
|
listen_port_mode = "manual"
|
|
advertise_endpoint = "http://192.168.200.61:$port"
|
|
advertise_transport = "direct_http"
|
|
connectivity_mode = "private_lan"
|
|
nat_type = "none"
|
|
region = "docker-test"
|
|
production_forwarding = $true
|
|
}
|
|
environment = @{}
|
|
} | Out-Null
|
|
}
|
|
|
|
function Clear-OldSmokeRouteIntents {
|
|
param(
|
|
[string]$SourceNodeID,
|
|
[string]$DestinationNodeID
|
|
)
|
|
$items = (Invoke-Api -Method GET -Path "/clusters/$ClusterID/mesh/route-intents?actor_user_id=$ActorUserID").route_intents
|
|
foreach ($item in @($items)) {
|
|
if ([string]$item.lifecycle_status -ne "active") {
|
|
continue
|
|
}
|
|
if ([string]$item.service_class -ne "vpn_packets") {
|
|
continue
|
|
}
|
|
if ([string]$item.source_selector.node_id -ne $SourceNodeID -or [string]$item.destination_selector.node_id -ne $DestinationNodeID) {
|
|
continue
|
|
}
|
|
$smoke = ""
|
|
if ($null -ne $item.policy -and $null -ne $item.policy.metadata) {
|
|
$prop = $item.policy.metadata.PSObject.Properties["smoke"]
|
|
if ($null -ne $prop) {
|
|
$smoke = [string]$prop.Value
|
|
}
|
|
}
|
|
if ($smoke -ne "c18z1_live_service_channel_ingress" -and $smoke -ne "c18z2_live_service_channel_soak" -and $smoke -ne "c18z3_live_service_channel_entry_ws_fallback" -and $smoke -ne "c18z4_live_service_channel_session_pressure" -and $smoke -ne "c18z5_live_service_channel_exit_restart" -and $smoke -ne "c18z6_live_service_channel_active_rebuild" -and $smoke -ne "c18z7_live_service_channel_concurrent_isolation" -and $smoke -ne "c18z8_live_service_channel_backpressure_isolation" -and $smoke -ne "c18z9_live_service_channel_route_pool" -and $smoke -ne "c18z10_live_service_channel_exit_pool" -and $smoke -ne "c18z11_live_service_channel_entry_pool") {
|
|
continue
|
|
}
|
|
Invoke-Api -Method POST -Path "/clusters/$ClusterID/mesh/route-intents/$($item.id)/expire" -Body @{ actor_user_id = $ActorUserID } | Out-Null
|
|
}
|
|
}
|
|
|
|
function New-RouteIntent {
|
|
param(
|
|
[string]$SourceNodeID,
|
|
[string]$DestinationNodeID,
|
|
[int]$Priority,
|
|
[string]$Label,
|
|
[string[]]$Hops,
|
|
[string]$EntryPoolID = "c18z11-entry-pool"
|
|
)
|
|
if ($null -eq $Hops -or $Hops.Count -eq 0) {
|
|
$Hops = @($SourceNodeID, $DestinationNodeID)
|
|
}
|
|
$expiresAt = (Get-Date).ToUniversalTime().AddMinutes(10).ToString("o")
|
|
return Invoke-Api -Method POST -Path "/clusters/$ClusterID/mesh/route-intents" -Body @{
|
|
actor_user_id = $ActorUserID
|
|
source_selector = @{ node_id = $SourceNodeID }
|
|
destination_selector = @{ node_id = $DestinationNodeID }
|
|
service_class = "vpn_packets"
|
|
priority = $Priority
|
|
policy = @{
|
|
synthetic_enabled = $true
|
|
route_version = "$runId-$Label"
|
|
policy_version = "$runId-$Label"
|
|
peer_directory_version = "$runId-$Label"
|
|
hops = @($Hops)
|
|
allowed_channels = @("vpn_packet", "fabric_control")
|
|
max_ttl = 8
|
|
max_hops = 8
|
|
expires_at = $expiresAt
|
|
metadata = @{
|
|
smoke = "c18z11_live_service_channel_entry_pool"
|
|
run_id = $runId
|
|
label = $Label
|
|
hop_count = $Hops.Count
|
|
entry_pool_id = $EntryPoolID
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Get-SyntheticConfig {
|
|
param([string]$NodeID)
|
|
return Invoke-Api -Method GET -Path "/clusters/$ClusterID/nodes/$NodeID/mesh/synthetic-config?actor_user_id=$ActorUserID"
|
|
}
|
|
|
|
function Get-LatestHeartbeat {
|
|
param([string]$NodeID)
|
|
return (Invoke-Api -Method GET -Path "/clusters/$ClusterID/nodes/$NodeID/heartbeats?actor_user_id=$ActorUserID&limit=1").heartbeats[0]
|
|
}
|
|
|
|
function Get-LatestRuntimeReport {
|
|
param([string]$NodeID)
|
|
$hb = Get-LatestHeartbeat -NodeID $NodeID
|
|
return @{
|
|
heartbeat = $hb
|
|
report = $hb.metadata.fabric_service_channel_runtime_report
|
|
}
|
|
}
|
|
|
|
function Wait-ForRuntimeReady {
|
|
param(
|
|
[string]$NodeID,
|
|
[int]$MinRoutes,
|
|
[int]$TimeoutSeconds = 90
|
|
)
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
do {
|
|
$latest = Get-LatestRuntimeReport -NodeID $NodeID
|
|
$report = $latest.report
|
|
if ($null -ne $report -and
|
|
$report.enabled -eq $true -and
|
|
$report.production_payload_forwarding -eq $true -and
|
|
[int]$report.route_candidate_total -ge $MinRoutes) {
|
|
return $latest
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
} while ((Get-Date) -lt $deadline)
|
|
throw "Timed out waiting for production service-channel runtime ready on node $NodeID"
|
|
}
|
|
|
|
function Wait-ForRuntimeConfigVersion {
|
|
param(
|
|
[string]$NodeID,
|
|
[string]$ConfigVersion,
|
|
[int]$TimeoutSeconds = 90
|
|
)
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
do {
|
|
$latest = Get-LatestRuntimeReport -NodeID $NodeID
|
|
if ($null -ne $latest.report) {
|
|
$loadedVersion = [string]$latest.report.config_version
|
|
if ($loadedVersion -ge $ConfigVersion) {
|
|
return $latest
|
|
}
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
} while ((Get-Date) -lt $deadline)
|
|
throw "Timed out waiting for node $NodeID to load synthetic config $ConfigVersion"
|
|
}
|
|
|
|
function Wait-ForRouteIntentVisible {
|
|
param(
|
|
[string]$NodeID,
|
|
[string[]]$RouteIDs,
|
|
[int]$TimeoutSeconds = 60
|
|
)
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
do {
|
|
$config = Get-SyntheticConfig -NodeID $NodeID
|
|
$routes = @($config.synthetic_mesh_config.routes)
|
|
$present = @($routes | Where-Object { $RouteIDs -contains $_.route_id })
|
|
if ($present.Count -ge $RouteIDs.Count) {
|
|
return $config
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
} while ((Get-Date) -lt $deadline)
|
|
throw "Timed out waiting for routes '$($RouteIDs -join ",")' in synthetic config for node $NodeID"
|
|
}
|
|
|
|
function Wait-ForRouteIntentNotVisible {
|
|
param(
|
|
[string]$NodeID,
|
|
[string]$RouteID,
|
|
[int]$TimeoutSeconds = 90
|
|
)
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
do {
|
|
$config = Get-SyntheticConfig -NodeID $NodeID
|
|
$routes = @($config.synthetic_mesh_config.routes)
|
|
$present = @($routes | Where-Object { $_.route_id -eq $RouteID })
|
|
if ($present.Count -eq 0) {
|
|
return $config
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
} while ((Get-Date) -lt $deadline)
|
|
throw "Timed out waiting for route '$RouteID' to disappear from synthetic config for node $NodeID"
|
|
}
|
|
|
|
function New-ServiceChannelLease {
|
|
param(
|
|
[string[]]$EntryNodeIDs,
|
|
[string]$ExitNodeID,
|
|
[string]$PreferredEntryNodeID,
|
|
[string]$VPNResourceID = $resourceId
|
|
)
|
|
if ($null -eq $EntryNodeIDs -or $EntryNodeIDs.Count -eq 0) {
|
|
throw "At least one entry node id is required"
|
|
}
|
|
if ([string]::IsNullOrWhiteSpace($PreferredEntryNodeID)) {
|
|
$PreferredEntryNodeID = [string]$EntryNodeIDs[0]
|
|
}
|
|
return (Invoke-Api -Method POST -Path "/clusters/$ClusterID/fabric/service-channels/leases" -Body @{
|
|
actor_user_id = $ActorUserID
|
|
organization_id = "org-c18z11-smoke"
|
|
user_id = $ActorUserID
|
|
resource_id = $VPNResourceID
|
|
service_class = "vpn_packets"
|
|
entry_node_ids = @($EntryNodeIDs)
|
|
exit_node_ids = @($ExitNodeID)
|
|
preferred_entry_node_id = $PreferredEntryNodeID
|
|
preferred_exit_node_id = $ExitNodeID
|
|
allowed_channels = @("vpn_packet", "bulk", "control")
|
|
ttl_seconds = 300
|
|
metadata = @{
|
|
smoke = "c18z11_live_service_channel_entry_pool"
|
|
run_id = $runId
|
|
}
|
|
}).fabric_service_channel_lease
|
|
}
|
|
|
|
function ConvertTo-Base64UrlJson {
|
|
param([object]$Value)
|
|
$json = $Value | ConvertTo-Json -Depth 80 -Compress
|
|
$bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
|
|
return [Convert]::ToBase64String($bytes).TrimEnd("=").Replace("+", "-").Replace("/", "_")
|
|
}
|
|
|
|
function Get-ObjectPropertyValue {
|
|
param(
|
|
[object]$Object,
|
|
[string]$Name
|
|
)
|
|
if ($null -eq $Object) {
|
|
return $null
|
|
}
|
|
$prop = $Object.PSObject.Properties[$Name]
|
|
if ($null -eq $prop) {
|
|
return $null
|
|
}
|
|
return $prop.Value
|
|
}
|
|
|
|
function New-TestIPv4UDPPacket {
|
|
param([int]$SourcePort)
|
|
$payload = [System.Text.Encoding]::ASCII.GetBytes("c18z1-$SourcePort")
|
|
$totalLength = 20 + 8 + $payload.Length
|
|
$packet = New-Object byte[] $totalLength
|
|
$packet[0] = 0x45
|
|
$packet[1] = 0
|
|
$packet[2] = [byte](($totalLength -shr 8) -band 0xff)
|
|
$packet[3] = [byte]($totalLength -band 0xff)
|
|
$packet[8] = 64
|
|
$packet[9] = 17
|
|
$packet[12] = 10; $packet[13] = 18; $packet[14] = 1; $packet[15] = 10
|
|
$packet[16] = 10; $packet[17] = 18; $packet[18] = 2; $packet[19] = 20
|
|
$udpOffset = 20
|
|
$destPort = 3389
|
|
$udpLength = 8 + $payload.Length
|
|
$packet[$udpOffset] = [byte](($SourcePort -shr 8) -band 0xff)
|
|
$packet[$udpOffset + 1] = [byte]($SourcePort -band 0xff)
|
|
$packet[$udpOffset + 2] = [byte](($destPort -shr 8) -band 0xff)
|
|
$packet[$udpOffset + 3] = [byte]($destPort -band 0xff)
|
|
$packet[$udpOffset + 4] = [byte](($udpLength -shr 8) -band 0xff)
|
|
$packet[$udpOffset + 5] = [byte]($udpLength -band 0xff)
|
|
[Array]::Copy($payload, 0, $packet, 28, $payload.Length)
|
|
return $packet
|
|
}
|
|
|
|
function New-PacketBatchBody {
|
|
param([byte[][]]$Packets)
|
|
$stream = [System.IO.MemoryStream]::new()
|
|
foreach ($packet in $Packets) {
|
|
$length = $packet.Length
|
|
$stream.WriteByte([byte](($length -shr 24) -band 0xff))
|
|
$stream.WriteByte([byte](($length -shr 16) -band 0xff))
|
|
$stream.WriteByte([byte](($length -shr 8) -band 0xff))
|
|
$stream.WriteByte([byte]($length -band 0xff))
|
|
$stream.Write($packet, 0, $packet.Length)
|
|
}
|
|
return $stream.ToArray()
|
|
}
|
|
|
|
function Invoke-ServiceChannelPost {
|
|
param(
|
|
[object]$Lease,
|
|
[int]$PortStart,
|
|
[string]$VPNResourceID = $resourceId
|
|
)
|
|
$packets = @()
|
|
for ($i = 0; $i -lt 8; $i++) {
|
|
$packets += ,(New-TestIPv4UDPPacket -SourcePort ($PortStart + $i))
|
|
}
|
|
$path = $Lease.entry_http.path_template.
|
|
Replace("{cluster_id}", $ClusterID).
|
|
Replace("{channel_id}", $Lease.channel_id).
|
|
Replace("{resource_id}", $VPNResourceID)
|
|
$url = "$EntryBaseUrl$path`?batch=true"
|
|
$headers = @{
|
|
"X-RAP-Service-Channel-Token" = $Lease.token.token
|
|
"X-RAP-Fabric-Channel-ID" = $Lease.channel_id
|
|
"X-RAP-Service-Class" = "vpn_packets"
|
|
"X-RAP-Channel-Class" = "vpn_packet"
|
|
"X-RAP-Service-Channel-Authority-Payload" = ConvertTo-Base64UrlJson -Value $Lease.authority_payload
|
|
"X-RAP-Service-Channel-Authority-Signature" = ConvertTo-Base64UrlJson -Value $Lease.authority_signature
|
|
}
|
|
$body = New-PacketBatchBody -Packets $packets
|
|
$client = [System.Net.Http.HttpClient]::new()
|
|
try {
|
|
$client.Timeout = [TimeSpan]::FromSeconds(30)
|
|
$request = [System.Net.Http.HttpRequestMessage]::new([System.Net.Http.HttpMethod]::Post, $url)
|
|
foreach ($header in $headers.GetEnumerator()) {
|
|
[void]$request.Headers.TryAddWithoutValidation($header.Key, [string]$header.Value)
|
|
}
|
|
$content = [System.Net.Http.ByteArrayContent]::new($body)
|
|
$content.Headers.ContentType = [System.Net.Http.Headers.MediaTypeHeaderValue]::Parse("application/vnd.rap.vpn-packet-batch.v1")
|
|
$request.Content = $content
|
|
$response = $client.SendAsync($request).GetAwaiter().GetResult()
|
|
$responseBody = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()
|
|
if (-not $response.IsSuccessStatusCode) {
|
|
throw "Service-channel POST $url failed with HTTP $([int]$response.StatusCode): $responseBody"
|
|
}
|
|
return [pscustomobject]@{
|
|
StatusCode = [int]$response.StatusCode
|
|
Body = $responseBody
|
|
}
|
|
}
|
|
finally {
|
|
$client.Dispose()
|
|
}
|
|
}
|
|
|
|
function Get-IngressSendPackets {
|
|
param([string]$NodeID)
|
|
$latest = Get-LatestRuntimeReport -NodeID $NodeID
|
|
$ingress = $latest.report.ingress
|
|
$sendPackets = Get-ObjectPropertyValue -Object $ingress -Name "send_packets"
|
|
if ($null -eq $sendPackets) {
|
|
return 0
|
|
}
|
|
return [int]$sendPackets
|
|
}
|
|
|
|
function Get-IngressRouteFailures {
|
|
param([string]$NodeID)
|
|
$latest = Get-LatestRuntimeReport -NodeID $NodeID
|
|
$ingress = $latest.report.ingress
|
|
$failures = Get-ObjectPropertyValue -Object $ingress -Name "send_route_failures"
|
|
if ($null -eq $failures) {
|
|
return 0
|
|
}
|
|
return [int]$failures
|
|
}
|
|
|
|
function Get-IngressFlowDropped {
|
|
param([string]$NodeID)
|
|
$latest = Get-LatestRuntimeReport -NodeID $NodeID
|
|
$ingress = $latest.report.ingress
|
|
if ($null -eq $ingress) {
|
|
return 0
|
|
}
|
|
$flowScheduler = Get-ObjectPropertyValue -Object $ingress -Name "flow_scheduler"
|
|
if ($null -eq $flowScheduler) {
|
|
return 0
|
|
}
|
|
$dropped = Get-ObjectPropertyValue -Object $flowScheduler -Name "dropped"
|
|
if ($null -eq $dropped) {
|
|
return 0
|
|
}
|
|
return [int]$dropped
|
|
}
|
|
|
|
function Get-ExitQueueDepth {
|
|
param(
|
|
[string]$NodeID,
|
|
[string]$VPNConnectionID
|
|
)
|
|
$latest = Get-LatestRuntimeReport -NodeID $NodeID
|
|
$queueKey = "$VPNConnectionID`:client_to_gateway"
|
|
$depths = $latest.report.inbox.queue_depths
|
|
if ($null -eq $depths) {
|
|
return 0
|
|
}
|
|
$prop = $depths.PSObject.Properties[$queueKey]
|
|
if ($null -eq $prop) {
|
|
return 0
|
|
}
|
|
return [int]$prop.Value
|
|
}
|
|
|
|
function Wait-ForExitQueueDepth {
|
|
param(
|
|
[string]$NodeID,
|
|
[string]$VPNConnectionID,
|
|
[int]$MinDepth,
|
|
[int]$TimeoutSeconds = 90
|
|
)
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
do {
|
|
$depth = Get-ExitQueueDepth -NodeID $NodeID -VPNConnectionID $VPNConnectionID
|
|
if ($depth -ge $MinDepth) {
|
|
return $depth
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
} while ((Get-Date) -lt $deadline)
|
|
throw "Timed out waiting for exit queue depth >= $MinDepth on node $NodeID"
|
|
}
|
|
|
|
function Invoke-ServiceChannelPostSafe {
|
|
param(
|
|
[object]$Lease,
|
|
[int]$PortStart,
|
|
[string]$VPNResourceID = $resourceId
|
|
)
|
|
try {
|
|
$response = Invoke-ServiceChannelPost -Lease $Lease -PortStart $PortStart -VPNResourceID $VPNResourceID
|
|
return [pscustomobject]@{
|
|
ok = $true
|
|
status_code = [int]$response.StatusCode
|
|
error = ""
|
|
}
|
|
}
|
|
catch {
|
|
return [pscustomobject]@{
|
|
ok = $false
|
|
status_code = 0
|
|
error = $_.Exception.Message
|
|
}
|
|
}
|
|
}
|
|
|
|
function ConvertTo-WebSocketURL {
|
|
param([string]$URL)
|
|
if ($URL.StartsWith("https://")) {
|
|
return "wss://" + $URL.Substring("https://".Length)
|
|
}
|
|
if ($URL.StartsWith("http://")) {
|
|
return "ws://" + $URL.Substring("http://".Length)
|
|
}
|
|
return $URL
|
|
}
|
|
|
|
function Invoke-ServiceChannelWebSocketSend {
|
|
param(
|
|
[object]$Lease,
|
|
[int]$PortStart,
|
|
[string]$VPNResourceID = $resourceId
|
|
)
|
|
$packets = @()
|
|
for ($i = 0; $i -lt 8; $i++) {
|
|
$packets += ,(New-TestIPv4UDPPacket -SourcePort ($PortStart + $i))
|
|
}
|
|
$path = $Lease.entry_http.websocket_path_template.
|
|
Replace("{cluster_id}", $ClusterID).
|
|
Replace("{channel_id}", $Lease.channel_id).
|
|
Replace("{resource_id}", $VPNResourceID)
|
|
$url = ConvertTo-WebSocketURL -URL "$EntryBaseUrl$path"
|
|
$socket = [System.Net.WebSockets.ClientWebSocket]::new()
|
|
$cts = [System.Threading.CancellationTokenSource]::new([TimeSpan]::FromSeconds(20))
|
|
try {
|
|
$null = $socket.Options.SetRequestHeader("X-RAP-Service-Channel-Token", [string]$Lease.token.token)
|
|
$null = $socket.Options.SetRequestHeader("X-RAP-Fabric-Channel-ID", [string]$Lease.channel_id)
|
|
$null = $socket.Options.SetRequestHeader("X-RAP-Service-Class", "vpn_packets")
|
|
$null = $socket.Options.SetRequestHeader("X-RAP-Channel-Class", "vpn_packet")
|
|
$null = $socket.Options.SetRequestHeader("X-RAP-Service-Channel-Authority-Payload", (ConvertTo-Base64UrlJson -Value $Lease.authority_payload))
|
|
$null = $socket.Options.SetRequestHeader("X-RAP-Service-Channel-Authority-Signature", (ConvertTo-Base64UrlJson -Value $Lease.authority_signature))
|
|
$null = $socket.ConnectAsync([Uri]$url, $cts.Token).GetAwaiter().GetResult()
|
|
$body = New-PacketBatchBody -Packets $packets
|
|
$segment = [ArraySegment[byte]]::new($body)
|
|
$null = $socket.SendAsync($segment, [System.Net.WebSockets.WebSocketMessageType]::Binary, $true, $cts.Token).GetAwaiter().GetResult()
|
|
Start-Sleep -Milliseconds 300
|
|
if ($socket.State -eq [System.Net.WebSockets.WebSocketState]::Open) {
|
|
$null = $socket.CloseOutputAsync([System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure, "c18z11 sent", $cts.Token).GetAwaiter().GetResult()
|
|
}
|
|
return [pscustomobject]@{
|
|
ok = $true
|
|
url = $url
|
|
sent_packets = $packets.Count
|
|
state = [string]$socket.State
|
|
error = ""
|
|
}
|
|
}
|
|
catch {
|
|
return [pscustomobject]@{
|
|
ok = $false
|
|
url = $url
|
|
sent_packets = 0
|
|
state = [string]$socket.State
|
|
error = $_.Exception.Message
|
|
}
|
|
}
|
|
finally {
|
|
$socket.Dispose()
|
|
$cts.Dispose()
|
|
}
|
|
}
|
|
|
|
function Invoke-ServiceChannelWebSocketPressure {
|
|
param(
|
|
[object]$Lease,
|
|
[int]$PortStart,
|
|
[int]$PreSwitchBatches,
|
|
[int]$PostSwitchBatches,
|
|
[int]$PacketsInBatch,
|
|
[int]$DelayMilliseconds,
|
|
[scriptblock]$AfterPreSwitchAction,
|
|
[string]$VPNResourceID = $resourceId
|
|
)
|
|
$path = $Lease.entry_http.websocket_path_template.
|
|
Replace("{cluster_id}", $ClusterID).
|
|
Replace("{channel_id}", $Lease.channel_id).
|
|
Replace("{resource_id}", $VPNResourceID)
|
|
$url = ConvertTo-WebSocketURL -URL "$EntryBaseUrl$path"
|
|
$socket = [System.Net.WebSockets.ClientWebSocket]::new()
|
|
$cts = [System.Threading.CancellationTokenSource]::new([TimeSpan]::FromSeconds(120))
|
|
$sentBatches = 0
|
|
$sentPackets = 0
|
|
$switchActionRan = $false
|
|
try {
|
|
$null = $socket.Options.SetRequestHeader("X-RAP-Service-Channel-Token", [string]$Lease.token.token)
|
|
$null = $socket.Options.SetRequestHeader("X-RAP-Fabric-Channel-ID", [string]$Lease.channel_id)
|
|
$null = $socket.Options.SetRequestHeader("X-RAP-Service-Class", "vpn_packets")
|
|
$null = $socket.Options.SetRequestHeader("X-RAP-Channel-Class", "vpn_packet")
|
|
$null = $socket.Options.SetRequestHeader("X-RAP-Service-Channel-Authority-Payload", (ConvertTo-Base64UrlJson -Value $Lease.authority_payload))
|
|
$null = $socket.Options.SetRequestHeader("X-RAP-Service-Channel-Authority-Signature", (ConvertTo-Base64UrlJson -Value $Lease.authority_signature))
|
|
$null = $socket.ConnectAsync([Uri]$url, $cts.Token).GetAwaiter().GetResult()
|
|
|
|
$totalBatches = $PreSwitchBatches + $PostSwitchBatches
|
|
for ($batch = 0; $batch -lt $totalBatches; $batch++) {
|
|
if ($batch -eq $PreSwitchBatches -and $null -ne $AfterPreSwitchAction) {
|
|
& $AfterPreSwitchAction
|
|
$switchActionRan = $true
|
|
}
|
|
$packets = @()
|
|
for ($i = 0; $i -lt $PacketsInBatch; $i++) {
|
|
$packets += ,(New-TestIPv4UDPPacket -SourcePort ($PortStart + ($batch * 100) + $i))
|
|
}
|
|
$body = New-PacketBatchBody -Packets $packets
|
|
$segment = [ArraySegment[byte]]::new($body)
|
|
$null = $socket.SendAsync($segment, [System.Net.WebSockets.WebSocketMessageType]::Binary, $true, $cts.Token).GetAwaiter().GetResult()
|
|
$sentBatches++
|
|
$sentPackets += $packets.Count
|
|
if ($DelayMilliseconds -gt 0) {
|
|
Start-Sleep -Milliseconds $DelayMilliseconds
|
|
}
|
|
}
|
|
Start-Sleep -Milliseconds 500
|
|
if ($socket.State -eq [System.Net.WebSockets.WebSocketState]::Open) {
|
|
$null = $socket.CloseOutputAsync([System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure, "c18z11 sent", $cts.Token).GetAwaiter().GetResult()
|
|
}
|
|
return [pscustomobject]@{
|
|
ok = $true
|
|
url = $url
|
|
sent_batches = $sentBatches
|
|
sent_packets = $sentPackets
|
|
switch_action_ran = $switchActionRan
|
|
state = [string]$socket.State
|
|
error = ""
|
|
}
|
|
}
|
|
catch {
|
|
return [pscustomobject]@{
|
|
ok = $false
|
|
url = $url
|
|
sent_batches = $sentBatches
|
|
sent_packets = $sentPackets
|
|
switch_action_ran = $switchActionRan
|
|
state = [string]$socket.State
|
|
error = $_.Exception.Message
|
|
}
|
|
}
|
|
finally {
|
|
$socket.Dispose()
|
|
$cts.Dispose()
|
|
}
|
|
}
|
|
|
|
function Send-BatchSeries {
|
|
param(
|
|
[object]$Lease,
|
|
[int]$Count,
|
|
[int]$PortBase,
|
|
[int]$DelayMilliseconds = 100,
|
|
[string]$VPNResourceID = $resourceId
|
|
)
|
|
$results = @()
|
|
for ($i = 0; $i -lt $Count; $i++) {
|
|
$results += Invoke-ServiceChannelPostSafe -Lease $Lease -PortStart ($PortBase + ($i * 100)) -VPNResourceID $VPNResourceID
|
|
if ($DelayMilliseconds -gt 0) {
|
|
Start-Sleep -Milliseconds $DelayMilliseconds
|
|
}
|
|
}
|
|
return $results
|
|
}
|
|
|
|
function Invoke-RemoteDocker {
|
|
param([string]$Command)
|
|
& ssh $DockerSSH $Command
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "ssh $DockerSSH command failed: $Command"
|
|
}
|
|
}
|
|
|
|
function Stop-TestUpdaters {
|
|
Invoke-RemoteDocker -Command "docker stop rap_host_agent_updater_test-1 rap_host_agent_updater_test-2 rap_host_agent_updater_test-3 >/dev/null 2>&1 || true"
|
|
}
|
|
|
|
function Start-TestUpdaters {
|
|
Invoke-RemoteDocker -Command "docker start rap_host_agent_updater_test-1 rap_host_agent_updater_test-2 rap_host_agent_updater_test-3 >/dev/null 2>&1 || true"
|
|
}
|
|
|
|
function Restart-ExitContainer {
|
|
param([string]$Name)
|
|
$containerName = "rap_test_node_" + $Name.Replace("-", "_")
|
|
Invoke-RemoteDocker -Command "docker restart $containerName >/dev/null"
|
|
}
|
|
|
|
function Restart-NodeContainer {
|
|
param([string]$Name)
|
|
$containerName = "rap_test_node_" + $Name.Replace("-", "_")
|
|
Invoke-RemoteDocker -Command "docker restart $containerName >/dev/null"
|
|
}
|
|
|
|
function Get-BackendClientGatewayDepth {
|
|
param([string]$VPNConnectionID)
|
|
$stats = (Invoke-Api -Method GET -Path "/clusters/$ClusterID/vpn-connections/$VPNConnectionID/tunnel/stats").vpn_packet_stats
|
|
$queue = $stats.client_to_gateway
|
|
if ($null -eq $queue) {
|
|
return 0
|
|
}
|
|
$depthProp = $queue.PSObject.Properties["queue_depth"]
|
|
if ($null -eq $depthProp) {
|
|
return 0
|
|
}
|
|
return [int]$depthProp.Value
|
|
}
|
|
|
|
function Wait-ForIngressRoute {
|
|
param(
|
|
[string]$NodeID,
|
|
[string]$RouteID,
|
|
[int]$MinSendPackets,
|
|
[int]$TimeoutSeconds = 45
|
|
)
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
do {
|
|
$latest = Get-LatestRuntimeReport -NodeID $NodeID
|
|
$ingress = $latest.report.ingress
|
|
$sendPackets = Get-ObjectPropertyValue -Object $ingress -Name "send_packets"
|
|
$selectedRoute = Get-ObjectPropertyValue -Object $ingress -Name "last_selected_route_id"
|
|
if ($null -ne $ingress -and
|
|
[int]$sendPackets -ge $MinSendPackets -and
|
|
[string]$selectedRoute -eq $RouteID) {
|
|
return $latest
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
} while ((Get-Date) -lt $deadline)
|
|
throw "Timed out waiting for ingress telemetry route=$RouteID packets>=$MinSendPackets on node $NodeID"
|
|
}
|
|
|
|
function Wait-ForIngressAnyRoute {
|
|
param(
|
|
[string]$NodeID,
|
|
[string[]]$RouteIDs,
|
|
[int]$MinSendPackets,
|
|
[int]$TimeoutSeconds = 45
|
|
)
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
do {
|
|
$latest = Get-LatestRuntimeReport -NodeID $NodeID
|
|
$ingress = $latest.report.ingress
|
|
$sendPackets = Get-ObjectPropertyValue -Object $ingress -Name "send_packets"
|
|
$selectedRoute = Get-ObjectPropertyValue -Object $ingress -Name "last_selected_route_id"
|
|
if ($null -ne $ingress -and
|
|
[int]$sendPackets -ge $MinSendPackets -and
|
|
$RouteIDs -contains [string]$selectedRoute) {
|
|
return $latest
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
} while ((Get-Date) -lt $deadline)
|
|
throw "Timed out waiting for ingress telemetry routes='$($RouteIDs -join ",")' packets>=$MinSendPackets on node $NodeID"
|
|
}
|
|
|
|
function Wait-ForExitInbox {
|
|
param(
|
|
[string]$NodeID,
|
|
[string]$VPNConnectionID,
|
|
[int]$TimeoutSeconds = 45
|
|
)
|
|
$queueKey = "$VPNConnectionID`:client_to_gateway"
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
do {
|
|
$latest = Get-LatestRuntimeReport -NodeID $NodeID
|
|
$depths = $latest.report.inbox.queue_depths
|
|
if ($null -ne $depths) {
|
|
$prop = $depths.PSObject.Properties[$queueKey]
|
|
if ($null -ne $prop -and [int]$prop.Value -gt 0) {
|
|
return $latest
|
|
}
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
} while ((Get-Date) -lt $deadline)
|
|
throw "Timed out waiting for exit inbox queue '$queueKey' on node $NodeID"
|
|
}
|
|
|
|
function Send-FeedbackHeartbeat {
|
|
param(
|
|
[string]$EntryNodeID,
|
|
[string]$BadRouteID,
|
|
[string]$GoodRouteID
|
|
)
|
|
return Invoke-Api -Method POST -Path "/clusters/$ClusterID/nodes/$EntryNodeID/heartbeats" -Body @{
|
|
health_status = "healthy"
|
|
reported_version = $RequiredNodeVersion
|
|
capabilities = @{
|
|
native_node_agent = $true
|
|
fabric_service_channel_runtime = $true
|
|
fabric_service_channel_route_manager = $true
|
|
smoke_feedback_injection = "c18z11"
|
|
}
|
|
service_states = @{ smoke = "c18z11_entry_pool_feedback" }
|
|
metadata = @{
|
|
fabric_service_channel_runtime_report = @{
|
|
schema_version = "c18l.fabric_service_channel_runtime_report.v1"
|
|
ingress = @{
|
|
flow_scheduler = @{
|
|
channel_stats = @{
|
|
"c18z11-entry-pool-flow" = @{
|
|
last_route_id = $GoodRouteID
|
|
last_failed_route_id = $BadRouteID
|
|
last_error = "c18z11 marked primary entry route stale before entry-pool lease refresh"
|
|
consecutive_failures = 3
|
|
stall_count = 1
|
|
last_send_duration_ms = 250
|
|
route_rebuild_recommended = $true
|
|
degraded_fallback_recommended = $false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
smoke = @{
|
|
name = "c18z11_live_service_channel_entry_pool"
|
|
run_id = $runId
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Wait-ForConfigDecision {
|
|
param(
|
|
[string]$NodeID,
|
|
[string]$BadRouteID,
|
|
[string]$ExpectedReplacementID,
|
|
[int]$TimeoutSeconds = 60
|
|
)
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
do {
|
|
$config = Get-SyntheticConfig -NodeID $NodeID
|
|
$decisions = @($config.synthetic_mesh_config.route_path_decisions.decisions)
|
|
$decision = @($decisions | Where-Object {
|
|
$_.route_id -eq $BadRouteID -and
|
|
$_.rebuild_status -eq "applied" -and
|
|
$_.replacement_route_id -eq $ExpectedReplacementID
|
|
}) | Select-Object -First 1
|
|
if ($null -ne $decision) {
|
|
return @{
|
|
config = $config
|
|
decision = $decision
|
|
}
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
} while ((Get-Date) -lt $deadline)
|
|
throw "Timed out waiting for applied rebuild decision $BadRouteID -> $ExpectedReplacementID"
|
|
}
|
|
|
|
function Wait-ForAppliedRebuildTransition {
|
|
param(
|
|
[string]$NodeID,
|
|
[string]$BadRouteID = "",
|
|
[string]$ReplacementRouteID = "",
|
|
[int]$TimeoutSeconds = 90
|
|
)
|
|
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
|
do {
|
|
$latest = Get-LatestRuntimeReport -NodeID $NodeID
|
|
$transition = $null
|
|
if ($null -ne $latest.report -and $null -ne $latest.report.ingress) {
|
|
$prop = $latest.report.ingress.PSObject.Properties["route_manager_transition"]
|
|
if ($null -ne $prop) {
|
|
$transition = $prop.Value
|
|
}
|
|
}
|
|
if ($null -ne $transition -and [string]$transition.status -eq "applied_rebuild") {
|
|
return $latest
|
|
}
|
|
if ($BadRouteID -ne "" -and $ReplacementRouteID -ne "") {
|
|
Send-FeedbackHeartbeat -EntryNodeID $NodeID -BadRouteID $BadRouteID -GoodRouteID $ReplacementRouteID | Out-Null
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
} while ((Get-Date) -lt $deadline)
|
|
throw "Timed out waiting for node route-manager transition applied_rebuild on node $NodeID"
|
|
}
|
|
|
|
$primaryEntryNode = Get-NodeByName -Name $PrimaryEntryNodeName
|
|
$alternateEntryNode = Get-NodeByName -Name $AlternateEntryNodeName
|
|
$exitNode = Get-NodeByName -Name $ExitNodeName
|
|
$primaryRouteID = ""
|
|
$alternateRouteID = ""
|
|
$updatersStopped = $false
|
|
$result = $null
|
|
|
|
try {
|
|
Stop-TestUpdaters
|
|
$updatersStopped = $true
|
|
|
|
Enable-TestMeshListener -Node $primaryEntryNode
|
|
Enable-TestMeshListener -Node $alternateEntryNode
|
|
Enable-TestMeshListener -Node $exitNode
|
|
Clear-OldSmokeRouteIntents -SourceNodeID $primaryEntryNode.id -DestinationNodeID $exitNode.id
|
|
Clear-OldSmokeRouteIntents -SourceNodeID $alternateEntryNode.id -DestinationNodeID $exitNode.id
|
|
|
|
$primaryIntent = New-RouteIntent `
|
|
-SourceNodeID $primaryEntryNode.id `
|
|
-DestinationNodeID $exitNode.id `
|
|
-Priority 2000000000 `
|
|
-Label "primary-entry" `
|
|
-Hops @($primaryEntryNode.id, $exitNode.id)
|
|
$alternateIntent = New-RouteIntent `
|
|
-SourceNodeID $alternateEntryNode.id `
|
|
-DestinationNodeID $exitNode.id `
|
|
-Priority 1999999990 `
|
|
-Label "alternate-entry" `
|
|
-Hops @($alternateEntryNode.id, $exitNode.id)
|
|
$primaryRouteID = $primaryIntent.route_intent.id
|
|
$alternateRouteID = $alternateIntent.route_intent.id
|
|
$routeIDs = @($primaryRouteID, $alternateRouteID)
|
|
|
|
$primaryEntryVisibleConfig = Wait-ForRouteIntentVisible -NodeID $primaryEntryNode.id -RouteIDs @($primaryRouteID)
|
|
$alternateEntryVisibleConfig = Wait-ForRouteIntentVisible -NodeID $alternateEntryNode.id -RouteIDs @($alternateRouteID)
|
|
$exitVisibleConfig = Wait-ForRouteIntentVisible -NodeID $exitNode.id -RouteIDs $routeIDs
|
|
$primaryEntryReadyBefore = Wait-ForRuntimeReady -NodeID $primaryEntryNode.id -MinRoutes 1
|
|
$alternateEntryReadyBefore = Wait-ForRuntimeReady -NodeID $alternateEntryNode.id -MinRoutes 1
|
|
$exitReadyBefore = Wait-ForRuntimeReady -NodeID $exitNode.id -MinRoutes 0
|
|
$primaryEntryLoadedConfig = Wait-ForRuntimeConfigVersion -NodeID $primaryEntryNode.id -ConfigVersion $primaryEntryVisibleConfig.synthetic_mesh_config.config_version
|
|
$alternateEntryLoadedConfig = Wait-ForRuntimeConfigVersion -NodeID $alternateEntryNode.id -ConfigVersion $alternateEntryVisibleConfig.synthetic_mesh_config.config_version
|
|
$exitLoadedConfig = Wait-ForRuntimeConfigVersion -NodeID $exitNode.id -ConfigVersion $exitVisibleConfig.synthetic_mesh_config.config_version
|
|
|
|
$initialLease = New-ServiceChannelLease -EntryNodeIDs @($primaryEntryNode.id, $alternateEntryNode.id) -ExitNodeID $exitNode.id -PreferredEntryNodeID $primaryEntryNode.id
|
|
if ($initialLease.status -ne "ready") {
|
|
throw "Initial lease status was '$($initialLease.status)', want ready"
|
|
}
|
|
if ([string]$initialLease.primary_route.route_id -ne $primaryRouteID -or [string]$initialLease.selected_entry_node_id -ne [string]$primaryEntryNode.id) {
|
|
throw "Initial lease should select primary entry route '$primaryRouteID': selected=$($initialLease.selected_entry_node_id) route=$($initialLease.primary_route.route_id)"
|
|
}
|
|
if ([string]$initialLease.selected_exit_node_id -ne [string]$exitNode.id -or @($initialLease.entry_pool).Count -lt 2) {
|
|
throw "Initial lease did not authorize expected entry pool: selected=$($initialLease.selected_entry_node_id) entry_pool_count=$(@($initialLease.entry_pool).Count)"
|
|
}
|
|
|
|
$baselinePrimarySendPackets = Get-IngressSendPackets -NodeID $primaryEntryNode.id
|
|
$baselineAlternateSendPackets = Get-IngressSendPackets -NodeID $alternateEntryNode.id
|
|
$baselinePrimaryRouteFailures = Get-IngressRouteFailures -NodeID $primaryEntryNode.id
|
|
$baselineAlternateRouteFailures = Get-IngressRouteFailures -NodeID $alternateEntryNode.id
|
|
$baselinePrimaryDropped = Get-IngressFlowDropped -NodeID $primaryEntryNode.id
|
|
$baselineAlternateDropped = Get-IngressFlowDropped -NodeID $alternateEntryNode.id
|
|
$baselineExitDepth = Get-ExitQueueDepth -NodeID $exitNode.id -VPNConnectionID $resourceId
|
|
$baselineBackendDepth = Get-BackendClientGatewayDepth -VPNConnectionID $resourceId
|
|
|
|
$EntryBaseUrl = Get-EntryBaseUrlForNode -Node $primaryEntryNode
|
|
$preResults = Send-BatchSeries -Lease $initialLease -Count $PreRebuildBatchCount -PortBase 61000 -DelayMilliseconds $BatchDelayMilliseconds
|
|
if (@($preResults | Where-Object { -not $_.ok }).Count -gt 0) {
|
|
throw "Primary entry pre-feedback send failed: $(@($preResults | Where-Object { -not $_.ok })[0].error)"
|
|
}
|
|
$preExitDepth = Wait-ForExitQueueDepth -NodeID $exitNode.id -VPNConnectionID $resourceId -MinDepth ($baselineExitDepth + ($PreRebuildBatchCount * $PacketsPerBatch)) -TimeoutSeconds 90
|
|
|
|
Send-FeedbackHeartbeat -EntryNodeID $primaryEntryNode.id -BadRouteID $primaryRouteID -GoodRouteID $alternateRouteID | Out-Null
|
|
Start-Sleep -Seconds 2
|
|
|
|
$replacementLease = New-ServiceChannelLease -EntryNodeIDs @($primaryEntryNode.id, $alternateEntryNode.id) -ExitNodeID $exitNode.id -PreferredEntryNodeID $primaryEntryNode.id
|
|
if ($replacementLease.status -ne "ready") {
|
|
throw "Replacement lease status was '$($replacementLease.status)', want ready"
|
|
}
|
|
if ([string]$replacementLease.primary_route.route_id -ne $alternateRouteID -or [string]$replacementLease.selected_entry_node_id -ne [string]$alternateEntryNode.id) {
|
|
throw "Replacement lease should select alternate entry route '$alternateRouteID': selected=$($replacementLease.selected_entry_node_id) route=$($replacementLease.primary_route.route_id)"
|
|
}
|
|
|
|
$EntryBaseUrl = Get-EntryBaseUrlForNode -Node $alternateEntryNode
|
|
$postResults = Send-BatchSeries -Lease $replacementLease -Count $PostRebuildBatchCount -PortBase 63000 -DelayMilliseconds $BatchDelayMilliseconds
|
|
if (@($postResults | Where-Object { -not $_.ok }).Count -gt 0) {
|
|
throw "Alternate entry post-feedback send failed: $(@($postResults | Where-Object { -not $_.ok })[0].error)"
|
|
}
|
|
$expectedPrePackets = $PreRebuildBatchCount * $PacketsPerBatch
|
|
$expectedPostPackets = $PostRebuildBatchCount * $PacketsPerBatch
|
|
$expectedPackets = $expectedPrePackets + $expectedPostPackets
|
|
$finalExitDepth = Wait-ForExitQueueDepth -NodeID $exitNode.id -VPNConnectionID $resourceId -MinDepth ($baselineExitDepth + $expectedPackets) -TimeoutSeconds 120
|
|
$postIngress = Wait-ForIngressRoute -NodeID $alternateEntryNode.id -RouteID $alternateRouteID -MinSendPackets ($baselineAlternateSendPackets + $expectedPostPackets) -TimeoutSeconds 120
|
|
|
|
$finalPrimaryEntryRuntime = Get-LatestRuntimeReport -NodeID $primaryEntryNode.id
|
|
$finalAlternateEntryRuntime = Get-LatestRuntimeReport -NodeID $alternateEntryNode.id
|
|
$finalExitRuntime = Get-LatestRuntimeReport -NodeID $exitNode.id
|
|
$finalPrimaryRouteFailures = Get-IngressRouteFailures -NodeID $primaryEntryNode.id
|
|
$finalAlternateRouteFailures = Get-IngressRouteFailures -NodeID $alternateEntryNode.id
|
|
$finalPrimaryDropped = Get-IngressFlowDropped -NodeID $primaryEntryNode.id
|
|
$finalAlternateDropped = Get-IngressFlowDropped -NodeID $alternateEntryNode.id
|
|
$finalBackendDepth = Get-BackendClientGatewayDepth -VPNConnectionID $resourceId
|
|
$feedbackExpire = Invoke-Api -Method POST -Path "/clusters/$ClusterID/fabric/service-channels/route-feedback/expire" -Body @{
|
|
actor_user_id = $ActorUserID
|
|
reporter_node_id = $primaryEntryNode.id
|
|
route_id = $primaryRouteID
|
|
service_class = "vpn_packets"
|
|
reason = "c18z11 entry pool smoke cleanup"
|
|
}
|
|
Start-Sleep -Seconds 2
|
|
$expiredAlternate = Invoke-Api -Method POST -Path "/clusters/$ClusterID/mesh/route-intents/$alternateRouteID/expire" -Body @{ actor_user_id = $ActorUserID }
|
|
$expiredPrimary = Invoke-Api -Method POST -Path "/clusters/$ClusterID/mesh/route-intents/$primaryRouteID/expire" -Body @{ actor_user_id = $ActorUserID }
|
|
|
|
$primaryRouteFailureDelta = $finalPrimaryRouteFailures - $baselinePrimaryRouteFailures
|
|
$alternateRouteFailureDelta = $finalAlternateRouteFailures - $baselineAlternateRouteFailures
|
|
$primaryDroppedDelta = $finalPrimaryDropped - $baselinePrimaryDropped
|
|
$alternateDroppedDelta = $finalAlternateDropped - $baselineAlternateDropped
|
|
$feedbackExpireStatus = Get-ObjectPropertyValue -Object (Get-ObjectPropertyValue -Object $feedbackExpire -Name "route_feedback_expire") -Name "status"
|
|
if ($null -eq $feedbackExpireStatus) {
|
|
$feedbackExpireStatus = "ok"
|
|
}
|
|
$initialAlternateRoutes = Get-ObjectPropertyValue -Object $initialLease -Name "alternate_routes"
|
|
$replacementAlternateRoutes = Get-ObjectPropertyValue -Object $replacementLease -Name "alternate_routes"
|
|
|
|
$result = [ordered]@{
|
|
schema_version = "c18z11.live_service_channel_entry_pool_smoke.v1"
|
|
run_id = $runId
|
|
base_url = $ApiBaseUrl
|
|
cluster_id = $ClusterID
|
|
primary_entry_node = @{ name = $primaryEntryNode.name; id = $primaryEntryNode.id; base_url = (Get-EntryBaseUrlForNode -Node $primaryEntryNode) }
|
|
alternate_entry_node = @{ name = $alternateEntryNode.name; id = $alternateEntryNode.id; base_url = (Get-EntryBaseUrlForNode -Node $alternateEntryNode) }
|
|
exit_node = @{ name = $exitNode.name; id = $exitNode.id }
|
|
resource_id = $resourceId
|
|
route_intents = @{
|
|
primary_entry_route_intent_id = $primaryRouteID
|
|
alternate_entry_route_intent_id = $alternateRouteID
|
|
primary_entry_hops = @($primaryEntryNode.id, $exitNode.id)
|
|
alternate_entry_hops = @($alternateEntryNode.id, $exitNode.id)
|
|
expired_primary_status = $expiredPrimary.route_intent.lifecycle_status
|
|
expired_alternate_status = $expiredAlternate.route_intent.lifecycle_status
|
|
}
|
|
initial_lease = @{
|
|
channel_id = $initialLease.channel_id
|
|
status = $initialLease.status
|
|
selected_entry_node_id = $initialLease.selected_entry_node_id
|
|
selected_exit_node_id = $initialLease.selected_exit_node_id
|
|
primary_route_id = $initialLease.primary_route.route_id
|
|
primary_route_hops = $initialLease.primary_route.hops
|
|
alternate_route_count = @($initialAlternateRoutes).Count
|
|
entry_pool_count = @($initialLease.entry_pool).Count
|
|
}
|
|
replacement_lease = @{
|
|
channel_id = $replacementLease.channel_id
|
|
status = $replacementLease.status
|
|
selected_entry_node_id = $replacementLease.selected_entry_node_id
|
|
selected_exit_node_id = $replacementLease.selected_exit_node_id
|
|
primary_route_id = $replacementLease.primary_route.route_id
|
|
primary_route_hops = $replacementLease.primary_route.hops
|
|
alternate_route_count = @($replacementAlternateRoutes).Count
|
|
entry_pool_count = @($replacementLease.entry_pool).Count
|
|
}
|
|
traffic = @{
|
|
pre_rebuild_batches = $PreRebuildBatchCount
|
|
post_rebuild_batches = $PostRebuildBatchCount
|
|
packets_per_batch = $PacketsPerBatch
|
|
expected_packets = $expectedPackets
|
|
expected_primary_entry_packets = $expectedPrePackets
|
|
expected_alternate_entry_packets = $expectedPostPackets
|
|
pre_send_ok = (@($preResults | Where-Object { $_.ok }).Count)
|
|
post_send_ok = (@($postResults | Where-Object { $_.ok }).Count)
|
|
}
|
|
route_failures = @{
|
|
primary_entry_delta = $primaryRouteFailureDelta
|
|
alternate_entry_delta = $alternateRouteFailureDelta
|
|
}
|
|
flow_drops = @{
|
|
primary_entry_delta = $primaryDroppedDelta
|
|
alternate_entry_delta = $alternateDroppedDelta
|
|
}
|
|
exit_queue = @{
|
|
baseline_depth = $baselineExitDepth
|
|
pre_feedback_depth = $preExitDepth
|
|
final_depth = $finalExitDepth
|
|
}
|
|
backend_fallback_queue = @{
|
|
baseline_depth = $baselineBackendDepth
|
|
depth = $finalBackendDepth
|
|
}
|
|
passed = $true
|
|
checks = [ordered]@{
|
|
primary_entry_production_forwarding_ready = ($primaryEntryReadyBefore.report.production_payload_forwarding -eq $true)
|
|
alternate_entry_production_forwarding_ready = ($alternateEntryReadyBefore.report.production_payload_forwarding -eq $true)
|
|
exit_production_forwarding_ready = ($exitReadyBefore.report.production_payload_forwarding -eq $true)
|
|
primary_entry_route_intent_visible = (@($primaryEntryVisibleConfig.synthetic_mesh_config.routes | Where-Object { $_.route_id -eq $primaryRouteID }).Count -ge 1)
|
|
alternate_entry_route_intent_visible = (@($alternateEntryVisibleConfig.synthetic_mesh_config.routes | Where-Object { $_.route_id -eq $alternateRouteID }).Count -ge 1)
|
|
exit_route_intents_visible = (@($exitVisibleConfig.synthetic_mesh_config.routes | Where-Object { $routeIDs -contains $_.route_id }).Count -ge 2)
|
|
primary_entry_runtime_loaded_visible_config = ([string]$primaryEntryLoadedConfig.report.config_version -ge [string]$primaryEntryVisibleConfig.synthetic_mesh_config.config_version)
|
|
alternate_entry_runtime_loaded_visible_config = ([string]$alternateEntryLoadedConfig.report.config_version -ge [string]$alternateEntryVisibleConfig.synthetic_mesh_config.config_version)
|
|
exit_runtime_loaded_visible_config = ([string]$exitLoadedConfig.report.config_version -ge [string]$exitVisibleConfig.synthetic_mesh_config.config_version)
|
|
initial_lease_selected_primary_entry = ([string]$initialLease.primary_route.route_id -eq $primaryRouteID -and [string]$initialLease.selected_entry_node_id -eq [string]$primaryEntryNode.id)
|
|
initial_lease_has_entry_pool = (@($initialLease.entry_pool).Count -ge 2)
|
|
replacement_lease_selected_alternate_entry = ([string]$replacementLease.primary_route.route_id -eq $alternateRouteID -and [string]$replacementLease.selected_entry_node_id -eq [string]$alternateEntryNode.id)
|
|
replacement_lease_has_entry_pool = (@($replacementLease.entry_pool).Count -ge 2)
|
|
pre_feedback_primary_entry_delivered = ($preExitDepth -ge ($baselineExitDepth + $expectedPrePackets))
|
|
post_feedback_alternate_entry_delivered = ($finalExitDepth -ge ($baselineExitDepth + $expectedPackets))
|
|
post_feedback_uses_alternate_entry_route = ([string]$postIngress.report.ingress.last_selected_route_id -eq $alternateRouteID)
|
|
no_backend_fallback_used = ($finalBackendDepth -eq $baselineBackendDepth)
|
|
no_flow_drops = ($primaryDroppedDelta -eq 0 -and $alternateDroppedDelta -eq 0)
|
|
route_intents_expired = ($expiredPrimary.route_intent.lifecycle_status -eq "expired" -and $expiredAlternate.route_intent.lifecycle_status -eq "expired")
|
|
}
|
|
telemetry = @{
|
|
final_primary_entry_ingress = $finalPrimaryEntryRuntime.report.ingress
|
|
final_alternate_entry_ingress = $finalAlternateEntryRuntime.report.ingress
|
|
final_exit_inbox = $finalExitRuntime.report.inbox
|
|
post_feedback_ingress = $postIngress.report.ingress
|
|
pre_results = $preResults
|
|
post_results = $postResults
|
|
}
|
|
}
|
|
|
|
$failedChecks = @($result.checks.GetEnumerator() | Where-Object { $_.Value -ne $true })
|
|
if ($failedChecks.Count -gt 0) {
|
|
throw "C18Z11 failed checks: $($failedChecks.Name -join ', ')"
|
|
}
|
|
}
|
|
finally {
|
|
if ($primaryRouteID) {
|
|
try { Invoke-Api -Method POST -Path "/clusters/$ClusterID/mesh/route-intents/$primaryRouteID/expire" -Body @{ actor_user_id = $ActorUserID } | Out-Null } catch {}
|
|
}
|
|
if ($alternateRouteID) {
|
|
try { Invoke-Api -Method POST -Path "/clusters/$ClusterID/mesh/route-intents/$alternateRouteID/expire" -Body @{ actor_user_id = $ActorUserID } | Out-Null } catch {}
|
|
}
|
|
if ($updatersStopped) {
|
|
try { Start-TestUpdaters } catch { Write-Warning "Could not restart test updaters: $($_.Exception.Message)" }
|
|
}
|
|
}
|
|
|
|
$resultFullPath = Join-Path $repoRoot $ResultPath
|
|
$resultDir = Split-Path -Parent $resultFullPath
|
|
if (-not (Test-Path $resultDir)) {
|
|
New-Item -ItemType Directory -Path $resultDir | Out-Null
|
|
}
|
|
$result | ConvertTo-Json -Depth 100 | Set-Content -Path $resultFullPath -Encoding UTF8
|
|
Write-Host "C18Z11 live service-channel entry pool smoke passed. Result: $resultFullPath"
|
|
$result
|