PR 7.2 — Retire legacy Galaxy projects + service

Matrix-gate satisfied (14 passed / 1 skipped / 0 failed on 2026-04-30
per docs/v2/Galaxy.ParityMatrix.md). Galaxy access flows through the
in-process GalaxyDriver → mxaccessgw exclusively. Legacy infrastructure
deleted in this commit:

Source projects (6):
- src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host         (.NET 4.8 x86 + MXAccess COM)
- src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy        (in-process pipe client)
- src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared       (pipe-IPC contracts)
- tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
- tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Proxy.Tests
- tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Tests

Test projects with no consumer after legacy retired (3):
- tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.E2E         (drove Galaxy.Host EXE)
- tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.ParityTests (drove both backends)
- tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport (only consumed by Host/Proxy tests)

Edits:
- ZB.MOM.WW.OtOpcUa.slnx: drop nine project entries
- Server.csproj: drop Driver.Galaxy.Proxy ProjectReference
- Server/Program.cs: drop GalaxyProxyDriverFactoryExtensions.Register
  + the parallel-registration comment block; only GalaxyDriverFactoryExtensions
  registers now under DriverType "GalaxyMxGateway"
- Install-Services.ps1: rewrite to drop OtOpcUaGalaxyHost service install +
  the GalaxySharedSecret/ZbConnection/GalaxyClientName/GalaxyPipeName/
  AvevaServiceDependencies/MxAccessInitialConnect* parameters that only
  applied to the legacy host. Adds a closing note pointing operators at
  the separate mxaccessgw install
- Uninstall-Services.ps1: keep OtOpcUaGalaxyHost in the cleanup loop so
  pre-7.2 rigs upgrade-uninstall cleanly, plus add OtOpcUaWonderwareHistorian
- scripts/e2e/test-galaxy.ps1: deleted (drove the legacy E2E)
- scripts/e2e/e2e-config.sample.json: rewrite the galaxy section comment
  to reflect the GalaxyMxGateway-only path
- scripts/e2e/README.md: drop OtOpcUaGalaxyHost references
- scripts/compliance/phase-7-compliance.ps1: drop Galaxy.Shared
  HistorianAlarms* checks (those contracts moved to
  Driver.Historian.Wonderware.Client in PR 3.4)

Live state: OtOpcUaGalaxyHost Windows service stopped + removed via
NSSM before this commit. The dev box's Galaxy access is now exclusively
through the running mxaccessgw (separate repo).

Stays out of scope for PR 7.2 (PR 7.3 territory):
- CLAUDE.md Galaxy section rewrite
- mxaccess_documentation.md deletion
- Memory entries for the now-retired Galaxy.Host service

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-04-30 08:01:19 -04:00
parent 6bf147a113
commit fe91d42927
117 changed files with 115 additions and 11754 deletions

View File

@@ -63,7 +63,9 @@ live driver. The factory-wiring block that originally gated stages
Live-boot verification:
- **Galaxy** — 7/7 stages (read / write / subscribe / alarms / history)
against a real Galaxy + `OtOpcUaGalaxyHost` on this dev box.
against a real Galaxy via the in-process `GalaxyDriver`
`mxaccessgw` (gRPC). PR 7.2 retired the legacy `OtOpcUaGalaxyHost`
out-of-process driver path.
- **AB CIP, S7** — 5/5 stages each under task #220 against the
`ab_server` + `python-snap7` fixtures.
- **AB Legacy** — 5/5 stages under task #222 against `ab_server` SLC500
@@ -155,7 +157,7 @@ section to skip it.
| Modbus | — | **PASS** (pymodbus fixture) |
| AB CIP | — | **PASS** (ab_server fixture) |
| AB Legacy | — | **PASS** (ab_server SLC500/MicroLogix/PLC-5 profiles; `/1,0` cip-path required for the Docker fixture) |
| Galaxy | — | **PASS** (requires OtOpcUaGalaxyHost + a live Galaxy; 7 stages including alarms + history) |
| Galaxy | — | **PASS** (requires mxaccessgw running + a live Galaxy; 7 stages including alarms + history; PR 7.2 retired the legacy OtOpcUaGalaxyHost path) |
| S7 | — | **PASS** (python-snap7 fixture) |
| FOCAS | `FOCAS_TRUST_WIRE=1` | **SKIP** (no public simulator — task #222 lab rig) |
| TwinCAT | `TWINCAT_TRUST_WIRE=1` | **SKIP** by default; features **validated** against the TCBSD VM fixture — set the env var to run |

View File

@@ -50,7 +50,7 @@
},
"galaxy": {
"$comment": "Galaxy (MXAccess) driver. Has no per-driver CLI — all stages go through otopcua-cli against the published NodeIds. Seven stages: probe / source read / virtual-tag bridge / subscribe-sees-change / reverse write / alarm fires / history read. PR 7.1 default-flipped backend to GalaxyMxGateway (in-process .NET 10 driver over mxaccessgw gRPC at http://localhost:5120 by default — override via the DriverInstance row's DriverConfig). Pre-flip rigs running the legacy 'Galaxy' DriverType still need OtOpcUaGalaxyHost running + seed-phase-7-smoke.sql applied with a real Galaxy attribute substituted into dbo.Tag.TagConfig.",
"$comment": "Galaxy (MXAccess) driver. Has no per-driver CLI — all stages go through otopcua-cli against the published NodeIds. Seven stages: probe / source read / virtual-tag bridge / subscribe-sees-change / reverse write / alarm fires / history read. The driver is now the in-process GalaxyDriver (DriverType = 'GalaxyMxGateway') talking gRPC to a separately-installed mxaccessgw at http://localhost:5120 by default — override via the DriverInstance row's DriverConfig. PR 7.2 retired the legacy 'Galaxy' DriverType + OtOpcUaGalaxyHost service.",
"sourceNodeId": "ns=2;s=p7-smoke-tag-source",
"virtualNodeId": "ns=2;s=p7-smoke-vt-derived",
"alarmNodeId": "ns=2;s=p7-smoke-al-overtemp",

View File

@@ -1,298 +0,0 @@
#Requires -Version 7.0
<#
.SYNOPSIS
End-to-end CLI test for the Galaxy (MXAccess) driver — read, write, subscribe,
alarms, and history through a running OtOpcUa server.
.DESCRIPTION
Unlike the other e2e scripts there is no `otopcua-galaxy-cli` — the Galaxy
driver proxy lives in-process with the server + talks to `OtOpcUaGalaxyHost`
over a named pipe (MXAccess is 32-bit COM, can't ship in the .NET 10 process).
Every stage therefore goes through `otopcua-cli` against the published OPC UA
address space.
Seven stages:
1. Probe — otopcua-cli connect + read the source NodeId; confirms
the whole Galaxy.Host → Proxy → server → client chain is
up
2. Source read — otopcua-cli read returns a Good value for the source
attribute; proves IReadable.ReadAsync is dispatching
through the IPC bridge
3. Virtual-tag bridge — `otopcua-cli read` on the VirtualTag NodeId; confirms
the Phase 7 CachedTagUpstreamSource is bridging the
driver-sourced input into the scripting engine
4. Subscribe-sees-change — subscribe to the source NodeId in the background;
Galaxy pushes a data-change event within N seconds
(Galaxy's underlying attribute must be actively
changing — production Galaxies typically have
scan-driven updates; for idle galaxies, widen
-ChangeWaitSec or drive the write stage below first)
5. Reverse bridge — `otopcua-cli write` to a writable Galaxy attribute;
read it back. Gracefully becomes INFO-only if the
attribute's Galaxy-side AccessLevel forbids writes
(BadUserAccessDenied / BadNotWritable)
6. Alarm fires — subscribe to the scripted-alarm Condition NodeId,
drive the source tag above its threshold, confirm an
Active alarm event surfaces. Exercises the Part 9
alarm-condition propagation path
7. History read — historyread on the source tag over the last hour;
confirms Aveva Historian → IHistoryProvider dispatch
returns samples
The Phase 7 seed (`scripts/smoke/seed-phase-7-smoke.sql`) already plants the
right shape — one Galaxy DriverInstance, one source Tag, one VirtualTag
(source × 2), one ScriptedAlarm (source > 50). Substitute the real Galaxy
attribute FullName into `dbo.Tag.TagConfig` before running.
.PARAMETER OpcUaUrl
OtOpcUa server endpoint. Default opc.tcp://localhost:4840.
.PARAMETER SourceNodeId
NodeId of the driver-sourced Galaxy tag (numeric, writable preferred). NodeIds
are path-based per OPC UA Part 3 §5.2.2 — the default matches the Phase 7 seed
walking `p7-smoke-galaxy` (DriverInstanceId) → `lab-floor` → `galaxy-line` →
`reactor-1` → `Source` (Tag.Name).
.PARAMETER VirtualNodeId
NodeId of the VirtualTag that computes MachineStatus = (Source > 0) (Phase 7
scripting). Same path-based scheme, ending in the VirtualTag.Name
(`MachineStatus`). The tag is historized so the write/subscribe exercise
doubles as a historian-sink check.
.PARAMETER AlarmNodeId
NodeId of the scripted-alarm Condition (fires when Source > 50). Same
path-based scheme, ending in ScriptedAlarm.Name (`OverTemp`).
.PARAMETER AlarmTriggerValue
Value written to -SourceNodeId to push it over the alarm threshold.
Default 75 (well above the seeded 50-threshold).
.PARAMETER ChangeWaitSec
Seconds the subscribe-sees-change stage waits for a natural data change.
Default 10. Idle galaxies may need this extended or the stage will fail
with "subscribe did not observe...".
.PARAMETER AlarmWaitSec
Seconds the alarm-fires stage waits after triggering the write. Default 10.
.PARAMETER HistoryLookbackSec
Seconds back from now to query history. Default 3600 (1 h).
.EXAMPLE
# Against the default Phase-7 smoke seed + live Galaxy + OtOpcUa server
./scripts/e2e/test-galaxy.ps1
.EXAMPLE
# Custom NodeIds from a non-smoke cluster
./scripts/e2e/test-galaxy.ps1 `
-SourceNodeId "ns=2;s=Reactor1.Temperature" `
-VirtualNodeId "ns=2;s=Reactor1.TempDoubled" `
-AlarmNodeId "ns=2;s=Reactor1.OverTemp" `
-AlarmTriggerValue 120
#>
param(
[string]$OpcUaUrl = "opc.tcp://localhost:4840",
[string]$SourceNodeId = "ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/Source",
[string]$VirtualNodeId = "ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/MachineStatus",
[string]$AlarmNodeId = "ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/OverTemp",
[string]$AlarmTriggerValue = "75",
[int]$ChangeWaitSec = 10,
[int]$AlarmWaitSec = 10,
[int]$HistoryLookbackSec = 3600,
# The default Phase 7 seed uses a Galaxy attribute with
# security_classification=Operate. Anonymous OPC UA sessions are denied writes
# against Operate-classified tags (PR 26 / docs/Security.md). Supply an LDAP
# user with WriteOperate to exercise the reverse-bridge stage — e.g.
# `-Username writeop -Password writeop123` against the dev-box GLAuth.
[string]$Username = "",
[string]$Password = ""
)
$ErrorActionPreference = "Stop"
. "$PSScriptRoot/_common.ps1"
$opcUaCli = Get-CliInvocation `
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Client.CLI" `
-ExeName "otopcua-cli"
# Auth-extension helper — appends `-U / -P` to the CLI args when credentials
# were supplied. Stays empty for anonymous runs so the default smoke path
# doesn't require an LDAP round-trip.
$authArgs = @()
if ($Username) { $authArgs += @("-U", $Username) }
if ($Password) { $authArgs += @("-P", $Password) }
$results = @()
# ---------------------------------------------------------------------------
# Stage 1 — Probe. The probe is an otopcua-cli read against the source NodeId;
# success implies Galaxy.Host is up + the pipe ACL lets the server connect +
# the Proxy is tracking the tag + the server published it.
# ---------------------------------------------------------------------------
Write-Header "Probe"
$probe = Invoke-Cli -Cli $opcUaCli -Args (@("read", "-u", $OpcUaUrl, "-n", $SourceNodeId) + $authArgs)
if ($probe.ExitCode -eq 0 -and $probe.Output -match "Status:\s+0x00000000") {
Write-Pass "source NodeId readable (Galaxy pipe → proxy → server → client chain up)"
$results += @{ Passed = $true }
} else {
Write-Fail "probe read failed (exit=$($probe.ExitCode))"
Write-Host $probe.Output
$results += @{ Passed = $false; Reason = "probe failed" }
}
# ---------------------------------------------------------------------------
# Stage 2 — Source read. Captures the current value for the later virtual-tag
# comparison + confirms read dispatch works end-to-end. Failure here without a
# stage-1 failure would be unusual — probe already reads.
# ---------------------------------------------------------------------------
Write-Header "Source read"
$sourceRead = Invoke-Cli -Cli $opcUaCli -Args (@("read", "-u", $OpcUaUrl, "-n", $SourceNodeId) + $authArgs)
$sourceValue = $null
if ($sourceRead.ExitCode -eq 0 -and $sourceRead.Output -match "Value:\s+([^\r\n]+)") {
$sourceValue = $Matches[1].Trim()
Write-Pass "source value = $sourceValue"
$results += @{ Passed = $true }
} else {
Write-Fail "source read failed"
Write-Host $sourceRead.Output
$results += @{ Passed = $false; Reason = "source read failed" }
}
# ---------------------------------------------------------------------------
# Stage 3 — Virtual-tag bridge. Reads the Phase 7 VirtualTag (source × 2). Not
# strictly driver-specific, but exercises the CachedTagUpstreamSource bridge
# (the seam most likely to silently stop working after a Galaxy-side change).
# Skip if the VirtualNodeId param is empty (non-Phase-7 clusters).
# ---------------------------------------------------------------------------
if ([string]::IsNullOrEmpty($VirtualNodeId)) {
Write-Header "Virtual-tag bridge"
Write-Skip "VirtualNodeId not supplied — skipping Phase 7 bridge check"
} else {
Write-Header "Virtual-tag bridge"
$vtRead = Invoke-Cli -Cli $opcUaCli -Args (@("read", "-u", $OpcUaUrl, "-n", $VirtualNodeId) + $authArgs)
if ($vtRead.ExitCode -eq 0 -and $vtRead.Output -match "Value:\s+([^\r\n]+)") {
$vtValue = $Matches[1].Trim()
Write-Pass "virtual-tag value = $vtValue (source was $sourceValue)"
$results += @{ Passed = $true }
} else {
Write-Fail "virtual-tag read failed"
Write-Host $vtRead.Output
$results += @{ Passed = $false; Reason = "virtual-tag read failed" }
}
}
# ---------------------------------------------------------------------------
# Stage 4 — Subscribe-sees-change. otopcua-cli subscribe in the background;
# wait N seconds for Galaxy to push any data-change event on the source node.
# This is optimistic — if the Galaxy attribute is idle, widen -ChangeWaitSec.
# ---------------------------------------------------------------------------
Write-Header "Subscribe sees change"
$stdout = New-TemporaryFile
$stderr = New-TemporaryFile
$subArgs = @($opcUaCli.PrefixArgs) + @(
"subscribe", "-u", $OpcUaUrl, "-n", $SourceNodeId,
"-i", "500", "--duration", "$ChangeWaitSec") + $authArgs
$subProc = Start-Process -FilePath $opcUaCli.File `
-ArgumentList $subArgs -NoNewWindow -PassThru `
-RedirectStandardOutput $stdout.FullName `
-RedirectStandardError $stderr.FullName
Write-Info "subscription started (pid $($subProc.Id)) for ${ChangeWaitSec}s"
$subProc.WaitForExit(($ChangeWaitSec + 5) * 1000) | Out-Null
if (-not $subProc.HasExited) { Stop-Process -Id $subProc.Id -Force }
$subOut = (Get-Content $stdout.FullName -Raw) + (Get-Content $stderr.FullName -Raw)
Remove-Item $stdout.FullName, $stderr.FullName -ErrorAction SilentlyContinue
# Any `=` followed by `(Good)` line after the initial subscribe-confirmation
# indicates at least one data-change tick arrived. The `@(...)` forces an array
# so `.Count` works on the 0-match + single-match cases that Set-StrictMode
# -Version 3.0 otherwise flags as `property 'Count' cannot be found`.
$changeLines = @(($subOut -split "`n") | Where-Object { $_ -match "=\s+.*\(Good\)" })
if ($changeLines.Count -gt 0) {
Write-Pass "$($changeLines.Count) data-change events observed"
$results += @{ Passed = $true }
} else {
Write-Fail "no data-change events in ${ChangeWaitSec}s — Galaxy attribute may be idle; rerun with -ChangeWaitSec larger, or trigger a change first"
Write-Host $subOut
$results += @{ Passed = $false; Reason = "no data-change" }
}
# ---------------------------------------------------------------------------
# Stage 5 — Reverse bridge (OPC UA write → Galaxy). Galaxy attributes with
# AccessLevel > FreeAccess often reject anonymous writes; record as INFO when
# that's the case rather than failing the whole script.
# ---------------------------------------------------------------------------
Write-Header "Reverse bridge (OPC UA write)"
$writeValue = [int]$AlarmTriggerValue # reuse the alarm trigger value — two stages for one write
$w = Invoke-Cli -Cli $opcUaCli -Args (@(
"write", "-u", $OpcUaUrl, "-n", $SourceNodeId, "-v", "$writeValue") + $authArgs)
if ($w.ExitCode -ne 0) {
# Connection/protocol failure — still a test failure.
Write-Fail "write CLI exit=$($w.ExitCode)"
Write-Host $w.Output
$results += @{ Passed = $false; Reason = "write failed" }
} elseif ($w.Output -match "Write failed:\s*0x801F0000") {
Write-Info "BadUserAccessDenied — attribute's Galaxy-side ACL blocks writes for this session. Not a bug; grant WriteOperate or run against a writable attribute."
$results += @{ Passed = $true; Reason = "acl-expected" }
} elseif ($w.Output -match "Write failed:\s*0x80390000|BadNotWritable") {
Write-Info "BadNotWritable — attribute is read-only at the Galaxy layer (status attributes, @-prefixed meta, etc)."
$results += @{ Passed = $true; Reason = "readonly-expected" }
} elseif ($w.Output -match "Write successful") {
# Read back — Galaxy poll interval + MXAccess advise may need a second or two to settle.
Start-Sleep -Seconds 2
$r = Invoke-Cli -Cli $opcUaCli -Args (@("read", "-u", $OpcUaUrl, "-n", $SourceNodeId) + $authArgs)
if ($r.Output -match "Value:\s+$([Regex]::Escape("$writeValue"))\b") {
Write-Pass "write propagated — source reads back $writeValue"
$results += @{ Passed = $true }
} else {
Write-Fail "write reported success but read-back did not reflect $writeValue"
Write-Host $r.Output
$results += @{ Passed = $false; Reason = "write-readback mismatch" }
}
} else {
Write-Fail "unexpected write response"
Write-Host $w.Output
$results += @{ Passed = $false; Reason = "unexpected write response" }
}
# ---------------------------------------------------------------------------
# Stage 6 — Alarm fires. Uses the helper from _common.ps1. If stage 5 already
# wrote the trigger value the alarm may already be active; that's fine — the
# Part 9 ConditionRefresh in the alarms CLI replays the current state so the
# subscribe window still captures the Active event.
# ---------------------------------------------------------------------------
if ([string]::IsNullOrEmpty($AlarmNodeId)) {
Write-Header "Alarm fires on threshold"
Write-Skip "AlarmNodeId not supplied — skipping alarm check"
} else {
$results += Test-AlarmFiresOnThreshold `
-OpcUaCli $opcUaCli `
-OpcUaUrl $OpcUaUrl `
-AlarmNodeId $AlarmNodeId `
-InputNodeId $SourceNodeId `
-TriggerValue $AlarmTriggerValue `
-DurationSec $AlarmWaitSec
}
# ---------------------------------------------------------------------------
# Stage 7 — History read. historyread against the source tag over the last N
# seconds. Failure modes the skip pattern catches: tag not historized in the
# Galaxy attribute's historization profile, or the lookback window misses the
# sample cadence.
# ---------------------------------------------------------------------------
$results += Test-HistoryHasSamples `
-OpcUaCli $opcUaCli `
-OpcUaUrl $OpcUaUrl `
-NodeId $SourceNodeId `
-LookbackSec $HistoryLookbackSec
Write-Summary -Title "Galaxy e2e" -Results $results
if ($results | Where-Object { -not $_.Passed }) { exit 1 }