Record project continuation changes
This commit is contained in:
@@ -0,0 +1,335 @@
|
||||
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]$ExitNodeName = "test-2",
|
||||
[string]$RequiredNodeVersion = "0.2.177",
|
||||
[string]$ResultPath = "artifacts\c18w-service-channel-route-manager-smoke-result.json"
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).ProviderPath
|
||||
$runId = "c18w-" + (Get-Date -Format "yyyyMMdd-HHmmss")
|
||||
|
||||
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 New-RouteIntent {
|
||||
param(
|
||||
[string]$SourceNodeID,
|
||||
[string]$DestinationNodeID,
|
||||
[int]$Priority,
|
||||
[string]$Label
|
||||
)
|
||||
$expiresAt = (Get-Date).ToUniversalTime().AddMinutes(8).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 = @($SourceNodeID, $DestinationNodeID)
|
||||
allowed_channels = @("vpn_packet", "fabric_control")
|
||||
max_ttl = 8
|
||||
max_hops = 8
|
||||
expires_at = $expiresAt
|
||||
metadata = @{
|
||||
smoke = "c18w_service_channel_route_manager"
|
||||
run_id = $runId
|
||||
label = $Label
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = "c18w"
|
||||
}
|
||||
service_states = @{
|
||||
smoke = "c18w_route_manager_feedback"
|
||||
}
|
||||
metadata = @{
|
||||
fabric_service_channel_runtime_report = @{
|
||||
schema_version = "c18l.fabric_service_channel_runtime_report.v1"
|
||||
ingress = @{
|
||||
flow_scheduler = @{
|
||||
channel_stats = @{
|
||||
"c18w-smoke-flow" = @{
|
||||
last_route_id = $GoodRouteID
|
||||
last_failed_route_id = $BadRouteID
|
||||
last_error = "c18w forced stale route feedback"
|
||||
consecutive_failures = 3
|
||||
stall_count = 0
|
||||
last_send_duration_ms = 250
|
||||
route_rebuild_recommended = $true
|
||||
degraded_fallback_recommended = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
smoke = @{
|
||||
name = "c18w_service_channel_route_manager"
|
||||
run_id = $runId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Send-CleanHeartbeat {
|
||||
param([string]$EntryNodeID)
|
||||
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 = "c18w-clean"
|
||||
}
|
||||
service_states = @{
|
||||
smoke = "c18w_route_manager_restore"
|
||||
}
|
||||
metadata = @{
|
||||
fabric_service_channel_runtime_report = @{
|
||||
schema_version = "c18l.fabric_service_channel_runtime_report.v1"
|
||||
ingress = @{
|
||||
flow_scheduler = @{
|
||||
channel_stats = @{}
|
||||
}
|
||||
}
|
||||
}
|
||||
smoke = @{
|
||||
name = "c18w_service_channel_route_manager"
|
||||
run_id = $runId
|
||||
phase = "clean_after_expire"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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-LatestTransition {
|
||||
param([string]$NodeID)
|
||||
$heartbeats = (Invoke-Api -Method GET -Path "/clusters/$ClusterID/nodes/$NodeID/heartbeats?actor_user_id=$ActorUserID&limit=8").heartbeats
|
||||
foreach ($heartbeat in $heartbeats) {
|
||||
$transition = $null
|
||||
$reportProperty = $heartbeat.metadata.PSObject.Properties["fabric_service_channel_runtime_report"]
|
||||
if ($null -ne $reportProperty) {
|
||||
$ingressProperty = $reportProperty.Value.PSObject.Properties["ingress"]
|
||||
if ($null -ne $ingressProperty) {
|
||||
$transitionProperty = $ingressProperty.Value.PSObject.Properties["route_manager_transition"]
|
||||
if ($null -ne $transitionProperty) {
|
||||
$transition = $transitionProperty.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($null -ne $transition -and $transition.schema_version -eq "rap.fabric_service_channel_route_manager_transition.v1") {
|
||||
return @{
|
||||
heartbeat = $heartbeat
|
||||
transition = $transition
|
||||
}
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Wait-ForTransitionStatus {
|
||||
param(
|
||||
[string]$NodeID,
|
||||
[string[]]$Statuses,
|
||||
[int]$TimeoutSeconds = 90
|
||||
)
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||
do {
|
||||
$latest = Get-LatestTransition -NodeID $NodeID
|
||||
if ($null -ne $latest -and $Statuses -contains [string]$latest.transition.status) {
|
||||
return $latest
|
||||
}
|
||||
Start-Sleep -Seconds 2
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
$last = Get-LatestTransition -NodeID $NodeID
|
||||
throw "Timed out waiting for transition status '$($Statuses -join ',')'. Last transition: $($last | ConvertTo-Json -Depth 20 -Compress)"
|
||||
}
|
||||
|
||||
function Wait-ForConfigDecision {
|
||||
param(
|
||||
[string]$NodeID,
|
||||
[string]$BadRouteID,
|
||||
[string]$ExpectedStatus,
|
||||
[int]$TimeoutSeconds = 45
|
||||
)
|
||||
$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 $ExpectedStatus }) | 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 rebuild_status=$ExpectedStatus for route $BadRouteID"
|
||||
}
|
||||
|
||||
function Wait-ForNoRebuildDecision {
|
||||
param(
|
||||
[string]$NodeID,
|
||||
[string]$BadRouteID,
|
||||
[int]$TimeoutSeconds = 45
|
||||
)
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||
do {
|
||||
$config = Get-SyntheticConfig -NodeID $NodeID
|
||||
$decisions = @($config.synthetic_mesh_config.route_path_decisions.decisions)
|
||||
$decision = @($decisions | Where-Object {
|
||||
$statusProperty = $_.PSObject.Properties["rebuild_status"]
|
||||
$_.route_id -eq $BadRouteID -and $null -ne $statusProperty -and -not [string]::IsNullOrWhiteSpace([string]$statusProperty.Value)
|
||||
}) | Select-Object -First 1
|
||||
if ($null -eq $decision) {
|
||||
$routeDecision = @($decisions | Where-Object { $_.route_id -eq $BadRouteID }) | Select-Object -First 1
|
||||
return @{
|
||||
config = $config
|
||||
decision = $routeDecision
|
||||
}
|
||||
}
|
||||
Start-Sleep -Seconds 2
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
throw "Timed out waiting for rebuild decision to clear for route $BadRouteID"
|
||||
}
|
||||
|
||||
Write-Host "C18W smoke $runId against $ApiBaseUrl"
|
||||
$entryNode = Get-NodeByName -Name $EntryNodeName
|
||||
$exitNode = Get-NodeByName -Name $ExitNodeName
|
||||
if ($entryNode.reported_version -ne $RequiredNodeVersion) {
|
||||
throw "$EntryNodeName reports version $($entryNode.reported_version), want $RequiredNodeVersion"
|
||||
}
|
||||
|
||||
Write-Host "Creating temporary service-channel route intents..."
|
||||
$badIntent = (New-RouteIntent -SourceNodeID $entryNode.id -DestinationNodeID $exitNode.id -Priority 9700 -Label "bad").route_intent
|
||||
$goodIntent = (New-RouteIntent -SourceNodeID $entryNode.id -DestinationNodeID $exitNode.id -Priority 9600 -Label "good").route_intent
|
||||
|
||||
Write-Host "Injecting fenced/healthy route feedback through heartbeat..."
|
||||
$feedbackHeartbeat = Send-FeedbackHeartbeat -EntryNodeID $entryNode.id -BadRouteID $badIntent.id -GoodRouteID $goodIntent.id
|
||||
$appliedDecision = Wait-ForConfigDecision -NodeID $entryNode.id -BadRouteID $badIntent.id -ExpectedStatus "applied"
|
||||
$appliedTransition = Wait-ForTransitionStatus -NodeID $entryNode.id -Statuses @("applied_rebuild")
|
||||
|
||||
Write-Host "Expiring route feedback and waiting for fresh config restore..."
|
||||
$expireResult = Invoke-Api -Method POST -Path "/clusters/$ClusterID/fabric/service-channels/route-feedback/expire" -Body @{
|
||||
actor_user_id = $ActorUserID
|
||||
reporter_node_id = $entryNode.id
|
||||
route_id = $badIntent.id
|
||||
service_class = "vpn_packets"
|
||||
reason = "c18w smoke restore by fresh config"
|
||||
}
|
||||
$cleanHeartbeat = Send-CleanHeartbeat -EntryNodeID $entryNode.id
|
||||
$restoredConfig = Wait-ForNoRebuildDecision -NodeID $entryNode.id -BadRouteID $badIntent.id
|
||||
$restoredTransition = Wait-ForTransitionStatus -NodeID $entryNode.id -Statuses @("restored_by_new_config", "empty")
|
||||
|
||||
$result = [ordered]@{
|
||||
schema_version = "c18w.service_channel_route_manager_smoke.v1"
|
||||
run_id = $runId
|
||||
api_base_url = $ApiBaseUrl
|
||||
cluster_id = $ClusterID
|
||||
actor_user_id = $ActorUserID
|
||||
entry_node = @{
|
||||
id = $entryNode.id
|
||||
name = $entryNode.name
|
||||
reported_version = $entryNode.reported_version
|
||||
}
|
||||
exit_node = @{
|
||||
id = $exitNode.id
|
||||
name = $exitNode.name
|
||||
reported_version = $exitNode.reported_version
|
||||
}
|
||||
routes = @{
|
||||
bad_route_id = $badIntent.id
|
||||
good_route_id = $goodIntent.id
|
||||
}
|
||||
feedback_heartbeat_id = $feedbackHeartbeat.heartbeat.id
|
||||
applied_decision = $appliedDecision.decision
|
||||
applied_transition = $appliedTransition.transition
|
||||
expire_result = $expireResult.route_feedback_expire
|
||||
clean_heartbeat_id = $cleanHeartbeat.heartbeat.id
|
||||
restored_config_generation = $restoredConfig.config.synthetic_mesh_config.config_version
|
||||
restored_decision = $restoredConfig.decision
|
||||
restored_transition = $restoredTransition.transition
|
||||
checks = @{
|
||||
applied_decision = $appliedDecision.decision.rebuild_status -eq "applied"
|
||||
applied_transition = $appliedTransition.transition.status -eq "applied_rebuild"
|
||||
feedback_expired = $expireResult.route_feedback_expire.expired_count -ge 1
|
||||
restored_config_has_no_rebuild_decision = $true
|
||||
restored_transition_seen = @("restored_by_new_config", "empty") -contains [string]$restoredTransition.transition.status
|
||||
}
|
||||
}
|
||||
|
||||
$resultDir = Split-Path -Parent (Join-Path $repoRoot $ResultPath)
|
||||
New-Item -ItemType Directory -Force -Path $resultDir | Out-Null
|
||||
$absoluteResultPath = Join-Path $repoRoot $ResultPath
|
||||
$result | ConvertTo-Json -Depth 100 | Set-Content -Encoding UTF8 $absoluteResultPath
|
||||
Write-Host "C18W smoke passed. Result: $absoluteResultPath"
|
||||
$result
|
||||
Reference in New Issue
Block a user