211 lines
8.4 KiB
PowerShell
211 lines
8.4 KiB
PowerShell
#Requires -Version 7.0
|
|
<#
|
|
.SYNOPSIS
|
|
End-to-end CLI test for AB CIP HSBY failover routing (PR abcip-5.2). Subscribes to
|
|
a tag through the OtOpcUa OPC UA server, flips the active chassis mid-stream via
|
|
the paired-fixture's hsby-mux sidecar HTTP endpoint, and asserts the subscribe
|
|
stream survives the failover (no permanent loss of notifications + the post-flip
|
|
data carries the partner-side update).
|
|
|
|
.DESCRIPTION
|
|
Paired-fixture variant of test-abcip.ps1. Where test-abcip.ps1 runs against a
|
|
single ab_server instance, this script assumes a paired fixture with two
|
|
ab_server instances (primary + partner) and an hsby-mux sidecar exposing
|
|
/flip {"active": "primary" | "partner"} over HTTP.
|
|
|
|
Five assertions:
|
|
- HsbyInitialActive — primary is Active at start (hsby-mux primes it)
|
|
- HsbyResolveActive — driver-diagnostics surfaces AbCip.HsbyActive == 1
|
|
- HsbyFailoverFlip — POST {"active": "partner"} → AbCip.HsbyActive == 2
|
|
- HsbySubscribeSurvives — subscribe stream stays open across the flip + sees
|
|
an updated value from the partner side
|
|
- HsbyFailoverCount — AbCip.HsbyFailoverCount increments by ≥ 1
|
|
|
|
.PARAMETER PrimaryGateway
|
|
ab://host[:port]/cip-path of the primary chassis. Default ab://127.0.0.1/1,0.
|
|
|
|
.PARAMETER PartnerGateway
|
|
ab://host[:port]/cip-path of the partner chassis. Default ab://127.0.0.2/1,0.
|
|
|
|
.PARAMETER HsbyMuxUrl
|
|
Base URL of the paired-fixture's hsby-mux sidecar. Default http://localhost:7080.
|
|
Endpoints used:
|
|
GET /role → returns {"primary":"Active","partner":"Standby"}
|
|
POST /flip {"active":"primary"|"partner"} → flips role tag values on each chassis
|
|
|
|
.PARAMETER OpcUaUrl
|
|
OtOpcUa server endpoint. Default opc.tcp://localhost:4840.
|
|
|
|
.PARAMETER BridgeNodeId
|
|
NodeId at which the server publishes the tag exercised by the subscribe assertion.
|
|
Required.
|
|
|
|
.PARAMETER TagPath
|
|
Logix symbolic path the bridge tag points at. Default 'TestDINT'.
|
|
|
|
.PARAMETER DriverInstanceId
|
|
DriverInstance ID for the AB CIP driver under test. Used to scope the
|
|
driver-diagnostics RPC. Default 'abcip-hsby'.
|
|
|
|
.EXAMPLE
|
|
./test-abcip-hsby.ps1 -BridgeNodeId 'ns=2;s=AbCip/Bridge/TestDINT'
|
|
#>
|
|
|
|
param(
|
|
[string]$PrimaryGateway = "ab://127.0.0.1/1,0",
|
|
[string]$PartnerGateway = "ab://127.0.0.2/1,0",
|
|
[string]$HsbyMuxUrl = "http://localhost:7080",
|
|
[string]$OpcUaUrl = "opc.tcp://localhost:4840",
|
|
[Parameter(Mandatory)] [string]$BridgeNodeId,
|
|
[string]$TagPath = "TestDINT",
|
|
[string]$DriverInstanceId = "abcip-hsby"
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
. "$PSScriptRoot/_common.ps1"
|
|
|
|
$abcipCli = Get-CliInvocation `
|
|
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli" `
|
|
-ExeName "otopcua-abcip-cli"
|
|
$opcUaCli = Get-CliInvocation `
|
|
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
|
-ExeName "otopcua-cli"
|
|
|
|
$results = @()
|
|
|
|
function Invoke-HsbyFlip {
|
|
param([string]$Active)
|
|
$body = @{ active = $Active } | ConvertTo-Json -Compress
|
|
try {
|
|
Invoke-RestMethod -Uri "$HsbyMuxUrl/flip" -Method Post -Body $body -ContentType 'application/json'
|
|
} catch {
|
|
throw "hsby-mux at $HsbyMuxUrl/flip rejected the request: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
function Get-HsbyDiagnosticValue {
|
|
param([string]$Counter)
|
|
# Pull driver-diagnostics through the OPC UA Admin RPC surface. The CLI returns
|
|
# a raw JSON blob; we grep out the named counter so the assertion is robust to
|
|
# other counters the driver surfaces.
|
|
$diagArgs = @($opcUaCli.PrefixArgs) + @(
|
|
"driver-diagnostics", "-u", $OpcUaUrl, "-d", $DriverInstanceId)
|
|
$diagOut = & $opcUaCli.File @diagArgs 2>&1
|
|
$joined = ($diagOut -join "`n")
|
|
if ($joined -match "${Counter}.*?:\s*([\d\.]+)") {
|
|
return [double]$matches[1]
|
|
}
|
|
return $null
|
|
}
|
|
|
|
# ---- HsbyInitialActive — hsby-mux primes primary as Active ----
|
|
Write-Header "HsbyInitialActive (POST $HsbyMuxUrl/flip {active=primary})"
|
|
try {
|
|
Invoke-HsbyFlip -Active "primary" | Out-Null
|
|
Start-Sleep -Seconds 3 # role-probe loop default tick is 2s
|
|
$active = Get-HsbyDiagnosticValue -Counter "AbCip.HsbyActive"
|
|
$passed = ($active -eq 1.0)
|
|
$results += [PSCustomObject]@{
|
|
Name = "HsbyInitialActive"
|
|
Passed = $passed
|
|
Detail = if ($passed) { "AbCip.HsbyActive=1 after priming primary" } else { "AbCip.HsbyActive=$active (expected 1)" }
|
|
}
|
|
} catch {
|
|
$results += [PSCustomObject]@{
|
|
Name = "HsbyInitialActive"; Passed = $false; Detail = $_.Exception.Message
|
|
}
|
|
}
|
|
|
|
# ---- HsbyResolveActive — driver routing reads through the primary ----
|
|
Write-Header "HsbyResolveActive (read $TagPath via primary)"
|
|
$readArgs = @("read") + @("-g", $PrimaryGateway, "-f", "ControlLogix") + @("-t", $TagPath, "--type", "DInt")
|
|
$readOut = & $abcipCli.Exe @($abcipCli.Args + $readArgs) 2>&1
|
|
$readOk = ($readOut -join "`n") -notmatch "(error|fail)"
|
|
$results += [PSCustomObject]@{
|
|
Name = "HsbyResolveActive"
|
|
Passed = $readOk
|
|
Detail = if ($readOk) { "primary read completed without error" } else { "read failed: $($readOut -join ' ')" }
|
|
}
|
|
|
|
# ---- HsbySubscribeSurvives + HsbyFailoverFlip + HsbyFailoverCount ----
|
|
Write-Header "HsbyFailoverFlip + HsbySubscribeSurvives (subscribe across flip)"
|
|
$failoverBaseline = Get-HsbyDiagnosticValue -Counter "AbCip.HsbyFailoverCount"
|
|
if ($null -eq $failoverBaseline) { $failoverBaseline = 0 }
|
|
|
|
$duration = 12
|
|
$subOut = New-TemporaryFile
|
|
$subErr = New-TemporaryFile
|
|
$subArgs = @($opcUaCli.PrefixArgs) + @(
|
|
"subscribe", "-u", $OpcUaUrl, "-n", $BridgeNodeId, "-i", "200", "--duration", "$duration")
|
|
$subProc = Start-Process -FilePath $opcUaCli.File -ArgumentList $subArgs `
|
|
-NoNewWindow -PassThru `
|
|
-RedirectStandardOutput $subOut.FullName `
|
|
-RedirectStandardError $subErr.FullName
|
|
|
|
# Let the subscribe settle + accumulate primary-side notifications.
|
|
Start-Sleep -Seconds 3
|
|
|
|
# Mid-stream flip — primary→Standby, partner→Active.
|
|
try {
|
|
Invoke-HsbyFlip -Active "partner" | Out-Null
|
|
} catch {
|
|
Stop-Process -Id $subProc.Id -Force -ErrorAction SilentlyContinue
|
|
$results += [PSCustomObject]@{
|
|
Name = "HsbyFailoverFlip"; Passed = $false; Detail = "hsby-mux flip rejected: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
# Wait for the role-probe loop to catch up (default tick 2s + ProbeIntervalMs slack).
|
|
Start-Sleep -Seconds 4
|
|
|
|
# Drive a write through the partner so the subscribe sees a fresh value.
|
|
$flipValue = Get-Random -Minimum 70000 -Maximum 79999
|
|
$writeArgs = @("write") + @("-g", $PartnerGateway, "-f", "ControlLogix") + @("-t", $TagPath, "--type", "DInt", "-v", $flipValue)
|
|
& $abcipCli.Exe @($abcipCli.Args + $writeArgs) | Out-Null
|
|
|
|
$activeAfter = Get-HsbyDiagnosticValue -Counter "AbCip.HsbyActive"
|
|
$flipPassed = ($activeAfter -eq 2.0)
|
|
$results += [PSCustomObject]@{
|
|
Name = "HsbyFailoverFlip"
|
|
Passed = $flipPassed
|
|
Detail = if ($flipPassed) { "AbCip.HsbyActive=2 after flip" } else { "AbCip.HsbyActive=$activeAfter (expected 2)" }
|
|
}
|
|
|
|
# Stop the subscribe + harvest the stream.
|
|
$subProc.WaitForExit(($duration + 5) * 1000) | Out-Null
|
|
if (-not $subProc.HasExited) { Stop-Process -Id $subProc.Id -Force }
|
|
|
|
$subText = (Get-Content $subOut.FullName -Raw) + (Get-Content $subErr.FullName -Raw)
|
|
Remove-Item $subOut.FullName, $subErr.FullName -ErrorAction SilentlyContinue
|
|
|
|
# Stream survival = at least one notification *after* the flip carries the new
|
|
# partner-side value. The post-flip write of $flipValue is the canary.
|
|
$saw = $subText -match "$flipValue"
|
|
$results += [PSCustomObject]@{
|
|
Name = "HsbySubscribeSurvives"
|
|
Passed = $saw
|
|
Detail = if ($saw) {
|
|
"subscribe stream surfaced post-flip value $flipValue from partner chassis"
|
|
} else {
|
|
"subscribe stream did not see the post-flip canary $flipValue — output: $subText"
|
|
}
|
|
}
|
|
|
|
# ---- HsbyFailoverCount — counter incremented by ≥ 1 ----
|
|
Write-Header "HsbyFailoverCount"
|
|
$failoverAfter = Get-HsbyDiagnosticValue -Counter "AbCip.HsbyFailoverCount"
|
|
if ($null -eq $failoverAfter) { $failoverAfter = 0 }
|
|
$counterOk = ($failoverAfter - $failoverBaseline) -ge 1
|
|
$results += [PSCustomObject]@{
|
|
Name = "HsbyFailoverCount"
|
|
Passed = $counterOk
|
|
Detail = if ($counterOk) {
|
|
"AbCip.HsbyFailoverCount went from $failoverBaseline → $failoverAfter"
|
|
} else {
|
|
"AbCip.HsbyFailoverCount unchanged ($failoverBaseline → $failoverAfter); expected at least 1 increment"
|
|
}
|
|
}
|
|
|
|
Write-Summary -Title "AB CIP HSBY failover e2e" -Results $results
|
|
if ($results | Where-Object { -not $_.Passed }) { exit 1 }
|