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:
@@ -73,13 +73,13 @@ Assert-TextFound "ScriptedAlarmSource implements IAlarmSource" "class ScriptedAl
|
||||
Assert-TextFound "IAlarmStateStore abstraction + in-memory default" "class InMemoryAlarmStateStore" @("src/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/IAlarmStateStore.cs")
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Stream D - Core.AlarmHistorian (SQLite store-and-forward + Galaxy.Host IPC contracts)"
|
||||
Write-Host "Stream D - Core.AlarmHistorian (SQLite store-and-forward; alarm-event sidecar IPC moved to Driver.Historian.Wonderware.Client in PR 3.4)"
|
||||
Assert-FileExists "Core.AlarmHistorian project" "src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian.csproj"
|
||||
Assert-TextFound "SqliteStoreAndForwardSink backoff ladder (1s..60s cap)" "BackoffLadder" @("src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs")
|
||||
Assert-TextFound "Default 1M row capacity + 30-day dead-letter retention (plan decision #21)" "DefaultDeadLetterRetention" @("src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs")
|
||||
Assert-TextFound "Per-event outcomes (Ack/RetryPlease/PermanentFail)" "HistorianWriteOutcome" @("src/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/IAlarmHistorianSink.cs")
|
||||
Assert-TextFound "Galaxy.Host IPC contract HistorianAlarmEventRequest" "class HistorianAlarmEventRequest" @("src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared/Contracts/HistorianAlarms.cs")
|
||||
Assert-TextFound "Historian connectivity status notification" "HistorianConnectivityStatusNotification" @("src/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared/Contracts/HistorianAlarms.cs")
|
||||
# Galaxy.Shared pipe-IPC contracts retired in PR 7.2 alongside the rest of the legacy
|
||||
# Galaxy projects. Wonderware sidecar contracts live in Driver.Historian.Wonderware.Client.
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Stream E - Config DB schema"
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
@@ -1,39 +1,52 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Registers the two v2 Windows services on a node: OtOpcUa (main server, net10) and
|
||||
OtOpcUaGalaxyHost (out-of-process Galaxy COM host, net48 x86).
|
||||
Registers the v2 Windows services on a node: OtOpcUa (main server, net10) and
|
||||
optionally OtOpcUaWonderwareHistorian (Wonderware historian sidecar).
|
||||
|
||||
.DESCRIPTION
|
||||
Phase 2 Stream D.2 — replaces the v1 single-service install (TopShelf-based OtOpcUa.Host).
|
||||
Installs both services with the correct service-account SID + per-process shared secret
|
||||
provisioning per `driver-stability.md §"IPC Security"`. Galaxy.Host depends on OtOpcUa
|
||||
(Galaxy.Host must be reachable when OtOpcUa starts; service dependency wiring + retry
|
||||
handled by OtOpcUa.Server NodeBootstrap).
|
||||
PR 7.2 retired the legacy out-of-process OtOpcUaGalaxyHost service alongside the
|
||||
GalaxyProxyDriver / GalaxyHost / GalaxyShared projects. Galaxy access now flows
|
||||
through the in-process GalaxyDriver talking gRPC to a separately-installed
|
||||
mxaccessgw. The mxaccessgw server runs out of its own repo
|
||||
(`c:\Users\dohertj2\Desktop\mxaccessgw\`) — see
|
||||
`docs/v2/Galaxy.ParityRig.md` for the gw setup recipe.
|
||||
|
||||
.PARAMETER InstallRoot
|
||||
Where the binaries live (typically C:\Program Files\OtOpcUa).
|
||||
|
||||
.PARAMETER ServiceAccount
|
||||
Service account SID or DOMAIN\name. Both services run under this account; the
|
||||
Galaxy.Host pipe ACL only allows this SID to connect (decision #76).
|
||||
Service account SID or DOMAIN\name. The OtOpcUa service runs under this account.
|
||||
|
||||
.PARAMETER GalaxySharedSecret
|
||||
Per-process secret passed to Galaxy.Host via env var. Generated freshly per install.
|
||||
.PARAMETER InstallWonderwareHistorian
|
||||
Gate the OtOpcUaWonderwareHistorian sidecar install. Off by default; set when
|
||||
the deployment uses the Wonderware historian for history reads + alarm-event
|
||||
persistence.
|
||||
|
||||
.PARAMETER ZbConnection
|
||||
Galaxy ZB SQL connection string (passed to Galaxy.Host via env var).
|
||||
.PARAMETER HistorianSharedSecret
|
||||
Per-process secret passed to the Historian sidecar via env var. Generated
|
||||
freshly per install when not supplied.
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-Services.ps1 -InstallRoot 'C:\Program Files\OtOpcUa' -ServiceAccount 'OTOPCUA\svc-otopcua'
|
||||
|
||||
.EXAMPLE
|
||||
.\Install-Services.ps1 -InstallRoot 'C:\Program Files\OtOpcUa' -ServiceAccount 'OTOPCUA\svc-otopcua' `
|
||||
-InstallWonderwareHistorian
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)] [string]$InstallRoot,
|
||||
[Parameter(Mandatory)] [string]$ServiceAccount,
|
||||
[string]$GalaxySharedSecret,
|
||||
[string]$ZbConnection = 'Server=localhost;Database=ZB;Integrated Security=True;TrustServerCertificate=True;Encrypt=False;',
|
||||
[string]$GalaxyClientName = 'OtOpcUa-Galaxy.Host',
|
||||
[string]$GalaxyPipeName = 'OtOpcUaGalaxy'
|
||||
|
||||
# PR 3.W — Wonderware historian sidecar. Optional; gates the
|
||||
# OtOpcUaWonderwareHistorian service. Secret + pipe defaults match the server's
|
||||
# Historian:Wonderware appsettings block.
|
||||
[switch]$InstallWonderwareHistorian,
|
||||
[string]$HistorianSharedSecret,
|
||||
[string]$HistorianPipeName = 'OtOpcUaWonderwareHistorian',
|
||||
[string]$HistorianServer = 'localhost',
|
||||
[int]$HistorianPort = 32568,
|
||||
[string[]]$AvevaServiceDependencies = @('NmxSvc', 'aaBootstrap', 'aaGR')
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
@@ -42,17 +55,18 @@ if (-not (Test-Path "$InstallRoot\OtOpcUa.Server.exe")) {
|
||||
Write-Error "OtOpcUa.Server.exe not found at $InstallRoot — copy the publish output first"
|
||||
exit 1
|
||||
}
|
||||
if (-not (Test-Path "$InstallRoot\Galaxy\OtOpcUa.Driver.Galaxy.Host.exe")) {
|
||||
Write-Error "OtOpcUa.Driver.Galaxy.Host.exe not found at $InstallRoot\Galaxy — copy the publish output first"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Generate a fresh shared secret per install if not supplied. Stored in DPAPI-protected file
|
||||
# rather than the registry so the service account can read it but other local users cannot.
|
||||
if (-not $GalaxySharedSecret) {
|
||||
# Generate fresh shared secrets per install if not supplied.
|
||||
function New-SharedSecret {
|
||||
$bytes = New-Object byte[] 32
|
||||
[System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)
|
||||
$GalaxySharedSecret = [Convert]::ToBase64String($bytes)
|
||||
return [Convert]::ToBase64String($bytes)
|
||||
}
|
||||
if ($InstallWonderwareHistorian -and -not $HistorianSharedSecret) { $HistorianSharedSecret = New-SharedSecret }
|
||||
|
||||
if ($InstallWonderwareHistorian -and -not (Test-Path "$InstallRoot\WonderwareHistorian\OtOpcUa.Driver.Historian.Wonderware.exe")) {
|
||||
Write-Error "OtOpcUa.Driver.Historian.Wonderware.exe not found at $InstallRoot\WonderwareHistorian — copy the publish output first"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Resolve the SID — the IPC ACL needs the SID, not the down-level name.
|
||||
@@ -62,41 +76,67 @@ $sid = if ($ServiceAccount.StartsWith('S-1-')) {
|
||||
(New-Object System.Security.Principal.NTAccount $ServiceAccount).Translate([System.Security.Principal.SecurityIdentifier]).Value
|
||||
}
|
||||
|
||||
# --- Install OtOpcUaGalaxyHost first (OtOpcUa starts after, depends on it being up).
|
||||
$galaxyEnv = @(
|
||||
"OTOPCUA_GALAXY_PIPE=$GalaxyPipeName"
|
||||
"OTOPCUA_ALLOWED_SID=$sid"
|
||||
"OTOPCUA_GALAXY_SECRET=$GalaxySharedSecret"
|
||||
"OTOPCUA_GALAXY_BACKEND=mxaccess"
|
||||
"OTOPCUA_GALAXY_ZB_CONN=$ZbConnection"
|
||||
"OTOPCUA_GALAXY_CLIENT_NAME=$GalaxyClientName"
|
||||
) -join "`0"
|
||||
$galaxyEnv += "`0`0"
|
||||
# --- Install OtOpcUaWonderwareHistorian (PR 3.W) — separate sidecar that exposes the
|
||||
# Wonderware Historian SDK via a named-pipe protocol consumed by the .NET 10 server.
|
||||
# Optional: only installed when -InstallWonderwareHistorian is supplied. Depends on the
|
||||
# hard AVEVA services that host the historian SDK runtime path.
|
||||
$historianDepend = $null
|
||||
if ($InstallWonderwareHistorian) {
|
||||
$historianEnv = @(
|
||||
"OTOPCUA_HISTORIAN_PIPE=$HistorianPipeName"
|
||||
"OTOPCUA_ALLOWED_SID=$sid"
|
||||
"OTOPCUA_HISTORIAN_SECRET=$HistorianSharedSecret"
|
||||
"OTOPCUA_HISTORIAN_ENABLED=true"
|
||||
"OTOPCUA_HISTORIAN_SERVER=$HistorianServer"
|
||||
"OTOPCUA_HISTORIAN_PORT=$HistorianPort"
|
||||
) -join "`0"
|
||||
$historianEnv += "`0`0"
|
||||
|
||||
Write-Host "Installing OtOpcUaGalaxyHost..."
|
||||
& sc.exe create OtOpcUaGalaxyHost binPath= "`"$InstallRoot\Galaxy\OtOpcUa.Driver.Galaxy.Host.exe`"" `
|
||||
DisplayName= 'OtOpcUa Galaxy Host (out-of-process MXAccess)' `
|
||||
start= auto `
|
||||
obj= $ServiceAccount | Out-Null
|
||||
Write-Host "Installing OtOpcUaWonderwareHistorian..."
|
||||
& sc.exe create OtOpcUaWonderwareHistorian binPath= "`"$InstallRoot\WonderwareHistorian\OtOpcUa.Driver.Historian.Wonderware.exe`"" `
|
||||
DisplayName= 'OtOpcUa Wonderware Historian Sidecar (out-of-process aahClient)' `
|
||||
start= auto `
|
||||
depend= ($AvevaServiceDependencies -join '/') `
|
||||
obj= $ServiceAccount | Out-Null
|
||||
& sc.exe config OtOpcUaWonderwareHistorian start= delayed-auto | Out-Null
|
||||
|
||||
# Set per-service environment variables via the registry — sc.exe doesn't expose them directly.
|
||||
$svcKey = "HKLM:\SYSTEM\CurrentControlSet\Services\OtOpcUaGalaxyHost"
|
||||
$envValue = $galaxyEnv.Split("`0") | Where-Object { $_ -ne '' }
|
||||
Set-ItemProperty -Path $svcKey -Name 'Environment' -Type MultiString -Value $envValue
|
||||
$svcKey = "HKLM:\SYSTEM\CurrentControlSet\Services\OtOpcUaWonderwareHistorian"
|
||||
$envValue = $historianEnv.Split("`0") | Where-Object { $_ -ne '' }
|
||||
Set-ItemProperty -Path $svcKey -Name 'Environment' -Type MultiString -Value $envValue
|
||||
|
||||
$historianDepend = 'OtOpcUaWonderwareHistorian'
|
||||
}
|
||||
|
||||
# --- Install OtOpcUa. Galaxy access flows through GalaxyDriver → mxaccessgw (gRPC),
|
||||
# so OtOpcUa no longer depends on a sibling service for Galaxy connectivity. The
|
||||
# mxaccessgw is installed separately. When the Wonderware sidecar is installed,
|
||||
# depend on it for startup ordering.
|
||||
$otOpcUaDepends = @()
|
||||
if ($historianDepend) { $otOpcUaDepends += $historianDepend }
|
||||
|
||||
# --- Install OtOpcUa (depends on Galaxy host being installed; doesn't strictly require it
|
||||
# started — OtOpcUa.Server NodeBootstrap retries on the IPC connect path).
|
||||
Write-Host "Installing OtOpcUa..."
|
||||
& sc.exe create OtOpcUa binPath= "`"$InstallRoot\OtOpcUa.Server.exe`"" `
|
||||
DisplayName= 'OtOpcUa Server' `
|
||||
start= auto `
|
||||
depend= 'OtOpcUaGalaxyHost' `
|
||||
obj= $ServiceAccount | Out-Null
|
||||
$createArgs = @(
|
||||
'create', 'OtOpcUa',
|
||||
'binPath=', "`"$InstallRoot\OtOpcUa.Server.exe`"",
|
||||
'DisplayName=', 'OtOpcUa Server',
|
||||
'start=', 'auto',
|
||||
'obj=', $ServiceAccount
|
||||
)
|
||||
if ($otOpcUaDepends.Count -gt 0) {
|
||||
$createArgs += @('depend=', ($otOpcUaDepends -join '/'))
|
||||
}
|
||||
& sc.exe @createArgs | Out-Null
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Installed. Start with:"
|
||||
Write-Host " sc.exe start OtOpcUaGalaxyHost"
|
||||
if ($InstallWonderwareHistorian) { Write-Host " sc.exe start OtOpcUaWonderwareHistorian" }
|
||||
Write-Host " sc.exe start OtOpcUa"
|
||||
if ($InstallWonderwareHistorian) {
|
||||
Write-Host ""
|
||||
Write-Host "Wonderware historian shared secret (configure into appsettings.json Historian:Wonderware:SharedSecret):"
|
||||
Write-Host " $HistorianSharedSecret"
|
||||
}
|
||||
Write-Host ""
|
||||
Write-Host "Galaxy shared secret (record this offline — required for service rebinding):"
|
||||
Write-Host " $GalaxySharedSecret"
|
||||
Write-Host "NOTE: Galaxy access flows through mxaccessgw — install + run that separately"
|
||||
Write-Host " per docs/v2/Galaxy.ParityRig.md. OtOpcUa connects via the Galaxy.Gateway"
|
||||
Write-Host " section of appsettings.json (default endpoint http://localhost:5120)."
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Stops + removes the two v2 services. Mirrors Install-Services.ps1.
|
||||
Stops + removes the v2 services. Mirrors Install-Services.ps1.
|
||||
|
||||
.DESCRIPTION
|
||||
PR 7.2 retired the legacy OtOpcUaGalaxyHost service. Galaxy access now flows
|
||||
through the in-process GalaxyDriver against a separately-installed mxaccessgw.
|
||||
OtOpcUaGalaxyHost is included in the cleanup loop below so this script safely
|
||||
removes it from any rig still carrying the legacy service from a pre-7.2
|
||||
install.
|
||||
#>
|
||||
[CmdletBinding()] param()
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
foreach ($svc in 'OtOpcUa', 'OtOpcUaGalaxyHost') {
|
||||
foreach ($svc in 'OtOpcUa', 'OtOpcUaWonderwareHistorian', 'OtOpcUaGalaxyHost') {
|
||||
if (Get-Service $svc -ErrorAction SilentlyContinue) {
|
||||
Write-Host "Stopping $svc..."
|
||||
Stop-Service $svc -Force -ErrorAction SilentlyContinue
|
||||
|
||||
Reference in New Issue
Block a user