305 lines
12 KiB
PowerShell
305 lines
12 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]$EntryNodeName = "test-1",
|
|
[string]$RelayNodeName = "test-3",
|
|
[string]$ExitNodeName = "test-2",
|
|
[string]$ResultPath = "artifacts\c18z12-service-channel-route-quality-smoke-result.json"
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).ProviderPath
|
|
$runId = "c18z12-" + (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-SmokeRouteLabel {
|
|
param([object]$RouteIntent)
|
|
if ($null -eq $RouteIntent -or $null -eq $RouteIntent.PSObject.Properties["policy"]) {
|
|
return ""
|
|
}
|
|
$policy = $RouteIntent.policy
|
|
if ($null -eq $policy -or $null -eq $policy.PSObject.Properties["metadata"]) {
|
|
return ""
|
|
}
|
|
$metadata = $policy.metadata
|
|
if ($null -eq $metadata) {
|
|
return ""
|
|
}
|
|
$smoke = $metadata.PSObject.Properties["smoke"]
|
|
if ($null -eq $smoke) {
|
|
return ""
|
|
}
|
|
return [string]$smoke.Value
|
|
}
|
|
|
|
function Clear-SmokeRouteIntents {
|
|
$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 ((Get-SmokeRouteLabel -RouteIntent $item) -ne "c18z12_service_channel_route_quality") {
|
|
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,
|
|
[string[]]$Hops,
|
|
[int]$Priority,
|
|
[string]$Label
|
|
)
|
|
$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 = "c18z12_service_channel_route_quality"
|
|
run_id = $runId
|
|
label = $Label
|
|
route_quality_smoke = $true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function New-ServiceChannelLease {
|
|
param(
|
|
[string]$EntryNodeID,
|
|
[string]$ExitNodeID
|
|
)
|
|
return (Invoke-Api -Method POST -Path "/clusters/$ClusterID/fabric/service-channels/leases" -Body @{
|
|
actor_user_id = $ActorUserID
|
|
organization_id = "org-c18z12-smoke"
|
|
user_id = $ActorUserID
|
|
resource_id = $resourceId
|
|
service_class = "vpn_packets"
|
|
entry_node_ids = @($EntryNodeID)
|
|
exit_node_ids = @($ExitNodeID)
|
|
preferred_entry_node_id = $EntryNodeID
|
|
preferred_exit_node_id = $ExitNodeID
|
|
allowed_channels = @("vpn_packet", "bulk", "control")
|
|
ttl_seconds = 300
|
|
metadata = @{
|
|
smoke = "c18z12_service_channel_route_quality"
|
|
run_id = $runId
|
|
}
|
|
}).fabric_service_channel_lease
|
|
}
|
|
|
|
function Send-QualityHeartbeat {
|
|
param(
|
|
[string]$EntryNodeID,
|
|
[string]$SlowRouteID,
|
|
[string]$FastRouteID
|
|
)
|
|
$observedAt = (Get-Date).ToUniversalTime().ToString("o")
|
|
return Invoke-Api -Method POST -Path "/clusters/$ClusterID/nodes/$EntryNodeID/heartbeats" -Body @{
|
|
health_status = "healthy"
|
|
reported_version = "0.2.185"
|
|
capabilities = @{
|
|
fabric_service_channel_route_manager = $true
|
|
fabric_service_channel_route_quality_feedback = $true
|
|
}
|
|
service_states = @{ smoke = "c18z12_route_quality_feedback" }
|
|
metadata = @{
|
|
fabric_service_channel_runtime_report = @{
|
|
schema_version = "c18l.fabric_service_channel_runtime_report.v1"
|
|
cluster_id = $ClusterID
|
|
local_node_id = $EntryNodeID
|
|
observed_at = $observedAt
|
|
ingress = @{
|
|
flow_scheduler = @{
|
|
schema_version = "rap.fabric_flow_scheduler.v1"
|
|
service_neutral = $true
|
|
service_mode = "application_protocol_agnostic"
|
|
channel_stats = @{
|
|
"quality-fast" = @{
|
|
last_route_id = $FastRouteID
|
|
last_next_hop = "fast"
|
|
last_send_duration_ms = 8
|
|
consecutive_failures = 0
|
|
stall_count = 0
|
|
route_rebuild_recommended = $false
|
|
degraded_fallback_recommended = $false
|
|
}
|
|
"quality-slow" = @{
|
|
last_route_id = $SlowRouteID
|
|
last_next_hop = "slow"
|
|
last_send_duration_ms = 900
|
|
consecutive_failures = 0
|
|
stall_count = 0
|
|
route_rebuild_recommended = $false
|
|
degraded_fallback_recommended = $false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$entryNode = Get-NodeByName -Name $EntryNodeName
|
|
$relayNode = Get-NodeByName -Name $RelayNodeName
|
|
$exitNode = Get-NodeByName -Name $ExitNodeName
|
|
$slowRouteID = ""
|
|
$fastRouteID = ""
|
|
$result = $null
|
|
|
|
try {
|
|
Clear-SmokeRouteIntents
|
|
$slowIntent = New-RouteIntent `
|
|
-SourceNodeID $entryNode.id `
|
|
-DestinationNodeID $exitNode.id `
|
|
-Hops @($entryNode.id, $relayNode.id, $exitNode.id) `
|
|
-Priority 2000000000 `
|
|
-Label "slow-high-priority"
|
|
$fastIntent = New-RouteIntent `
|
|
-SourceNodeID $entryNode.id `
|
|
-DestinationNodeID $exitNode.id `
|
|
-Hops @($entryNode.id, $exitNode.id) `
|
|
-Priority 1999999950 `
|
|
-Label "fast-lower-priority"
|
|
$slowRouteID = $slowIntent.route_intent.id
|
|
$fastRouteID = $fastIntent.route_intent.id
|
|
|
|
$initialLease = New-ServiceChannelLease -EntryNodeID $entryNode.id -ExitNodeID $exitNode.id
|
|
if ($initialLease.status -ne "ready" -or [string]$initialLease.primary_route.route_id -ne $slowRouteID) {
|
|
throw "Initial lease should select higher-priority slow route '$slowRouteID': status=$($initialLease.status) route=$($initialLease.primary_route.route_id)"
|
|
}
|
|
|
|
$qualityHeartbeat = Send-QualityHeartbeat -EntryNodeID $entryNode.id -SlowRouteID $slowRouteID -FastRouteID $fastRouteID
|
|
Start-Sleep -Seconds 2
|
|
$qualityLease = New-ServiceChannelLease -EntryNodeID $entryNode.id -ExitNodeID $exitNode.id
|
|
if ($qualityLease.status -ne "ready" -or [string]$qualityLease.primary_route.route_id -ne $fastRouteID) {
|
|
throw "Quality lease should select fast route '$fastRouteID': status=$($qualityLease.status) route=$($qualityLease.primary_route.route_id) score=$($qualityLease.primary_route.path_score)"
|
|
}
|
|
|
|
$expiredSlow = Invoke-Api -Method POST -Path "/clusters/$ClusterID/mesh/route-intents/$slowRouteID/expire" -Body @{ actor_user_id = $ActorUserID }
|
|
$expiredFast = Invoke-Api -Method POST -Path "/clusters/$ClusterID/mesh/route-intents/$fastRouteID/expire" -Body @{ actor_user_id = $ActorUserID }
|
|
|
|
$result = [ordered]@{
|
|
schema_version = "c18z12.service_channel_route_quality_smoke.v1"
|
|
run_id = $runId
|
|
base_url = $ApiBaseUrl
|
|
cluster_id = $ClusterID
|
|
entry_node = @{ name = $entryNode.name; id = $entryNode.id }
|
|
relay_node = @{ name = $relayNode.name; id = $relayNode.id }
|
|
exit_node = @{ name = $exitNode.name; id = $exitNode.id }
|
|
resource_id = $resourceId
|
|
route_intents = @{
|
|
slow_route_id = $slowRouteID
|
|
fast_route_id = $fastRouteID
|
|
slow_hops = @($entryNode.id, $relayNode.id, $exitNode.id)
|
|
fast_hops = @($entryNode.id, $exitNode.id)
|
|
expired_slow_status = $expiredSlow.route_intent.lifecycle_status
|
|
expired_fast_status = $expiredFast.route_intent.lifecycle_status
|
|
}
|
|
initial_lease = @{
|
|
status = $initialLease.status
|
|
primary_route_id = $initialLease.primary_route.route_id
|
|
primary_path_score = $initialLease.primary_route.path_score
|
|
score_reasons = $initialLease.primary_route.score_reasons
|
|
}
|
|
quality_lease = @{
|
|
status = $qualityLease.status
|
|
primary_route_id = $qualityLease.primary_route.route_id
|
|
primary_path_score = $qualityLease.primary_route.path_score
|
|
score_reasons = $qualityLease.primary_route.score_reasons
|
|
}
|
|
feedback = @{
|
|
heartbeat_status = $qualityHeartbeat.heartbeat.health_status
|
|
fast_last_send_duration_ms = 8
|
|
slow_last_send_duration_ms = 900
|
|
}
|
|
passed = $true
|
|
checks = [ordered]@{
|
|
initial_prefers_high_priority_slow_route = ([string]$initialLease.primary_route.route_id -eq $slowRouteID)
|
|
quality_prefers_fast_route = ([string]$qualityLease.primary_route.route_id -eq $fastRouteID)
|
|
fast_route_has_quality_reason = (@($qualityLease.primary_route.score_reasons | Where-Object { $_ -eq "service_channel_quality_latency_le_10ms" }).Count -ge 1)
|
|
route_intents_expired = ($expiredSlow.route_intent.lifecycle_status -eq "expired" -and $expiredFast.route_intent.lifecycle_status -eq "expired")
|
|
}
|
|
}
|
|
$failedChecks = @($result.checks.GetEnumerator() | Where-Object { $_.Value -ne $true })
|
|
if ($failedChecks.Count -gt 0) {
|
|
throw "C18Z12 failed checks: $($failedChecks.Name -join ', ')"
|
|
}
|
|
}
|
|
finally {
|
|
if ($slowRouteID) {
|
|
try { Invoke-Api -Method POST -Path "/clusters/$ClusterID/mesh/route-intents/$slowRouteID/expire" -Body @{ actor_user_id = $ActorUserID } | Out-Null } catch {}
|
|
}
|
|
if ($fastRouteID) {
|
|
try { Invoke-Api -Method POST -Path "/clusters/$ClusterID/mesh/route-intents/$fastRouteID/expire" -Body @{ actor_user_id = $ActorUserID } | Out-Null } catch {}
|
|
}
|
|
}
|
|
|
|
$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 "C18Z12 service-channel route quality smoke passed. Result: $resultFullPath"
|
|
$result
|