Galaxy E2E — point at live writable historized attribute + MachineStatus
Pick a Galaxy attribute that actually exercises the full driver stack: TestMachine_001.TestHistoryValue. Verified against the live dev-box ZB: it's Int32, writable (security_classification = Operate), and historized (HistoryExtension primitive). The query lives in `gr/queries/attributes_extended.sql` — swap to any other writable historized attribute via the same shape (`WHERE is_historized = 1 AND security_classification > 0`). Seed changes: - Tag row: FullName = TestMachine_001.TestHistoryValue (Int32 / ReadWrite) - VirtualTag renamed: `Doubled` → `MachineStatus` (Boolean), script returns `Source > 0`. Historized, so the write/subscribe exercise doubles as a historian-sink check once the alarm/write stages are enabled. - Scripted alarm predicate reads the same Source and fires on `> 50`. - Added ClusterNodeCredential(sa → p7-smoke-node) row so sp_GetCurrentGenerationForCluster's caller-binding check passes. Without this the server bootstrap fails with `Unauthorized: caller sa is not bound to NodeId p7-smoke-node`. E2E script: - Path-based NodeId defaults updated to match the new MachineStatus virtual tag. - Added optional `-Username / -Password` parameters. Anonymous sessions still get denied against Operate-classified attributes (PR 26 / docs/Security.md); supplying `-Username writeop -Password writeop123` against the dev-box GLAuth exercises the reverse-bridge stage. - Wired those credentials into every Invoke-Cli / Start-Process CLI invocation the script drives. Anonymous smoke remains 3/7 pass (probe + source read + reverse-bridge marked acl-expected INFO). A fuller run with `-Username writeop -Password writeop123` requires also enabling LDAP + a SecurityProfile that carries a UserName UserTokenPolicy — separate config step tracked alongside #124 (3-user authz matrix). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -55,8 +55,10 @@
|
|||||||
`reactor-1` → `Source` (Tag.Name).
|
`reactor-1` → `Source` (Tag.Name).
|
||||||
|
|
||||||
.PARAMETER VirtualNodeId
|
.PARAMETER VirtualNodeId
|
||||||
NodeId of the VirtualTag computed as Source × 2 (Phase 7 scripting). Same
|
NodeId of the VirtualTag that computes MachineStatus = (Source > 0) (Phase 7
|
||||||
path-based scheme, ending in the VirtualTag.Name (`Doubled`).
|
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
|
.PARAMETER AlarmNodeId
|
||||||
NodeId of the scripted-alarm Condition (fires when Source > 50). Same
|
NodeId of the scripted-alarm Condition (fires when Source > 50). Same
|
||||||
@@ -93,12 +95,19 @@
|
|||||||
param(
|
param(
|
||||||
[string]$OpcUaUrl = "opc.tcp://localhost:4840",
|
[string]$OpcUaUrl = "opc.tcp://localhost:4840",
|
||||||
[string]$SourceNodeId = "ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/Source",
|
[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/Doubled",
|
[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]$AlarmNodeId = "ns=2;s=p7-smoke-galaxy/lab-floor/galaxy-line/reactor-1/OverTemp",
|
||||||
[string]$AlarmTriggerValue = "75",
|
[string]$AlarmTriggerValue = "75",
|
||||||
[int]$ChangeWaitSec = 10,
|
[int]$ChangeWaitSec = 10,
|
||||||
[int]$AlarmWaitSec = 10,
|
[int]$AlarmWaitSec = 10,
|
||||||
[int]$HistoryLookbackSec = 3600
|
[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"
|
$ErrorActionPreference = "Stop"
|
||||||
@@ -108,6 +117,13 @@ $opcUaCli = Get-CliInvocation `
|
|||||||
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
||||||
-ExeName "otopcua-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 = @()
|
$results = @()
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -117,7 +133,7 @@ $results = @()
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
Write-Header "Probe"
|
Write-Header "Probe"
|
||||||
$probe = Invoke-Cli -Cli $opcUaCli -Args @("read", "-u", $OpcUaUrl, "-n", $SourceNodeId)
|
$probe = Invoke-Cli -Cli $opcUaCli -Args (@("read", "-u", $OpcUaUrl, "-n", $SourceNodeId) + $authArgs)
|
||||||
if ($probe.ExitCode -eq 0 -and $probe.Output -match "Status:\s+0x00000000") {
|
if ($probe.ExitCode -eq 0 -and $probe.Output -match "Status:\s+0x00000000") {
|
||||||
Write-Pass "source NodeId readable (Galaxy pipe → proxy → server → client chain up)"
|
Write-Pass "source NodeId readable (Galaxy pipe → proxy → server → client chain up)"
|
||||||
$results += @{ Passed = $true }
|
$results += @{ Passed = $true }
|
||||||
@@ -134,7 +150,7 @@ if ($probe.ExitCode -eq 0 -and $probe.Output -match "Status:\s+0x00000000") {
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
Write-Header "Source read"
|
Write-Header "Source read"
|
||||||
$sourceRead = Invoke-Cli -Cli $opcUaCli -Args @("read", "-u", $OpcUaUrl, "-n", $SourceNodeId)
|
$sourceRead = Invoke-Cli -Cli $opcUaCli -Args (@("read", "-u", $OpcUaUrl, "-n", $SourceNodeId) + $authArgs)
|
||||||
$sourceValue = $null
|
$sourceValue = $null
|
||||||
if ($sourceRead.ExitCode -eq 0 -and $sourceRead.Output -match "Value:\s+([^\r\n]+)") {
|
if ($sourceRead.ExitCode -eq 0 -and $sourceRead.Output -match "Value:\s+([^\r\n]+)") {
|
||||||
$sourceValue = $Matches[1].Trim()
|
$sourceValue = $Matches[1].Trim()
|
||||||
@@ -158,7 +174,7 @@ if ([string]::IsNullOrEmpty($VirtualNodeId)) {
|
|||||||
Write-Skip "VirtualNodeId not supplied — skipping Phase 7 bridge check"
|
Write-Skip "VirtualNodeId not supplied — skipping Phase 7 bridge check"
|
||||||
} else {
|
} else {
|
||||||
Write-Header "Virtual-tag bridge"
|
Write-Header "Virtual-tag bridge"
|
||||||
$vtRead = Invoke-Cli -Cli $opcUaCli -Args @("read", "-u", $OpcUaUrl, "-n", $VirtualNodeId)
|
$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]+)") {
|
if ($vtRead.ExitCode -eq 0 -and $vtRead.Output -match "Value:\s+([^\r\n]+)") {
|
||||||
$vtValue = $Matches[1].Trim()
|
$vtValue = $Matches[1].Trim()
|
||||||
Write-Pass "virtual-tag value = $vtValue (source was $sourceValue)"
|
Write-Pass "virtual-tag value = $vtValue (source was $sourceValue)"
|
||||||
@@ -181,7 +197,7 @@ $stdout = New-TemporaryFile
|
|||||||
$stderr = New-TemporaryFile
|
$stderr = New-TemporaryFile
|
||||||
$subArgs = @($opcUaCli.PrefixArgs) + @(
|
$subArgs = @($opcUaCli.PrefixArgs) + @(
|
||||||
"subscribe", "-u", $OpcUaUrl, "-n", $SourceNodeId,
|
"subscribe", "-u", $OpcUaUrl, "-n", $SourceNodeId,
|
||||||
"-i", "500", "--duration", "$ChangeWaitSec")
|
"-i", "500", "--duration", "$ChangeWaitSec") + $authArgs
|
||||||
$subProc = Start-Process -FilePath $opcUaCli.File `
|
$subProc = Start-Process -FilePath $opcUaCli.File `
|
||||||
-ArgumentList $subArgs -NoNewWindow -PassThru `
|
-ArgumentList $subArgs -NoNewWindow -PassThru `
|
||||||
-RedirectStandardOutput $stdout.FullName `
|
-RedirectStandardOutput $stdout.FullName `
|
||||||
@@ -214,8 +230,8 @@ if ($changeLines.Count -gt 0) {
|
|||||||
|
|
||||||
Write-Header "Reverse bridge (OPC UA write)"
|
Write-Header "Reverse bridge (OPC UA write)"
|
||||||
$writeValue = [int]$AlarmTriggerValue # reuse the alarm trigger value — two stages for one write
|
$writeValue = [int]$AlarmTriggerValue # reuse the alarm trigger value — two stages for one write
|
||||||
$w = Invoke-Cli -Cli $opcUaCli -Args @(
|
$w = Invoke-Cli -Cli $opcUaCli -Args (@(
|
||||||
"write", "-u", $OpcUaUrl, "-n", $SourceNodeId, "-v", "$writeValue")
|
"write", "-u", $OpcUaUrl, "-n", $SourceNodeId, "-v", "$writeValue") + $authArgs)
|
||||||
if ($w.ExitCode -ne 0) {
|
if ($w.ExitCode -ne 0) {
|
||||||
# Connection/protocol failure — still a test failure.
|
# Connection/protocol failure — still a test failure.
|
||||||
Write-Fail "write CLI exit=$($w.ExitCode)"
|
Write-Fail "write CLI exit=$($w.ExitCode)"
|
||||||
@@ -230,7 +246,7 @@ if ($w.ExitCode -ne 0) {
|
|||||||
} elseif ($w.Output -match "Write successful") {
|
} elseif ($w.Output -match "Write successful") {
|
||||||
# Read back — Galaxy poll interval + MXAccess advise may need a second or two to settle.
|
# Read back — Galaxy poll interval + MXAccess advise may need a second or two to settle.
|
||||||
Start-Sleep -Seconds 2
|
Start-Sleep -Seconds 2
|
||||||
$r = Invoke-Cli -Cli $opcUaCli -Args @("read", "-u", $OpcUaUrl, "-n", $SourceNodeId)
|
$r = Invoke-Cli -Cli $opcUaCli -Args (@("read", "-u", $OpcUaUrl, "-n", $SourceNodeId) + $authArgs)
|
||||||
if ($r.Output -match "Value:\s+$([Regex]::Escape("$writeValue"))\b") {
|
if ($r.Output -match "Value:\s+$([Regex]::Escape("$writeValue"))\b") {
|
||||||
Write-Pass "write propagated — source reads back $writeValue"
|
Write-Pass "write propagated — source reads back $writeValue"
|
||||||
$results += @{ Passed = $true }
|
$results += @{ Passed = $true }
|
||||||
|
|||||||
@@ -71,6 +71,13 @@ INSERT dbo.ClusterNode(NodeId, ClusterId, RedundancyRole, Host, OpcUaPort, Dashb
|
|||||||
VALUES (@NodeId, @ClusterId, 'Primary', 'localhost', 4840, 5000,
|
VALUES (@NodeId, @ClusterId, 'Primary', 'localhost', 4840, 5000,
|
||||||
'urn:OtOpcUa:p7-smoke-node', 200, 1, 'p7-smoke');
|
'urn:OtOpcUa:p7-smoke-node', 200, 1, 'p7-smoke');
|
||||||
|
|
||||||
|
-- sp_GetCurrentGenerationForCluster gates access by SUSER_SNAME() against
|
||||||
|
-- ClusterNodeCredential; without this binding the Server bootstrap fails with
|
||||||
|
-- `Unauthorized: caller sa is not bound to NodeId p7-smoke-node`. Dev Docker
|
||||||
|
-- SQL runs with `sa`; production deploys would rotate to a per-node login.
|
||||||
|
INSERT dbo.ClusterNodeCredential(NodeId, Kind, Value, Enabled, CreatedBy)
|
||||||
|
VALUES (@NodeId, N'SqlLogin', N'sa', 1, N'p7-smoke');
|
||||||
|
|
||||||
-- 2. Generation (created Draft, flipped to Published at the end so insert order
|
-- 2. Generation (created Draft, flipped to Published at the end so insert order
|
||||||
-- constraints (one Draft per cluster, etc.) don't fight us).
|
-- constraints (one Draft per cluster, etc.) don't fight us).
|
||||||
DECLARE @Gen bigint;
|
DECLARE @Gen bigint;
|
||||||
@@ -106,30 +113,43 @@ VALUES (@Gen, @DrvId, @ClusterId, @NsId, 'galaxy-smoke', 'Galaxy', N'{
|
|||||||
}', 1);
|
}', 1);
|
||||||
|
|
||||||
-- 6. One driver-sourced Tag bound to the Equipment. TagConfig is the Galaxy
|
-- 6. One driver-sourced Tag bound to the Equipment. TagConfig is the Galaxy
|
||||||
-- fullRef ("DelmiaReceiver_001.DownloadPath" style); replace with a real
|
-- fullRef; the EquipmentNodeWalker reads the JSON's FullName field and
|
||||||
-- attribute on this Galaxy. The script paths below use
|
-- hands that string to the Galaxy driver as the MXAccess reference.
|
||||||
-- /lab-floor/galaxy-line/reactor-1/Source which the EquipmentNodeWalker
|
-- Default points at TestMachine_001.TestHistoryValue — the live dev-box
|
||||||
-- emits + the DriverSubscriptionBridge maps to this driver fullRef.
|
-- Galaxy ships it as Int32 + writable (security_classification=Operate)
|
||||||
|
-- + historized (HistoryExtension primitive), so the E2E script can
|
||||||
|
-- exercise read + write + subscribe + alarm + history against the
|
||||||
|
-- same attribute. Swap to any other writable historized attribute on
|
||||||
|
-- this Galaxy by re-running `gr/queries/attributes_extended.sql` with
|
||||||
|
-- `WHERE is_historized=1 AND security_classification > 0`.
|
||||||
INSERT dbo.Tag(GenerationId, TagId, DriverInstanceId, EquipmentId, Name, DataType,
|
INSERT dbo.Tag(GenerationId, TagId, DriverInstanceId, EquipmentId, Name, DataType,
|
||||||
AccessLevel, TagConfig, WriteIdempotent)
|
AccessLevel, TagConfig, WriteIdempotent)
|
||||||
VALUES (@Gen, @TagId, @DrvId, @EqId, 'Source', 'Float64', 'Read',
|
VALUES (@Gen, @TagId, @DrvId, @EqId, 'Source', 'Int32', 'ReadWrite',
|
||||||
N'{"FullName":"REPLACE_WITH_REAL_GALAXY_ATTRIBUTE","DataType":"Float64"}', 0);
|
N'{"FullName":"TestMachine_001.TestHistoryValue","DataType":"Int32"}', 0);
|
||||||
|
|
||||||
-- 7. Scripts (SourceHash is SHA-256 of SourceCode, computed externally — using
|
-- 7. Scripts (SourceHash is SHA-256 of SourceCode, computed externally — using
|
||||||
-- a placeholder here; the engine recomputes on first use anyway).
|
-- a placeholder here; the engine recomputes on first use anyway).
|
||||||
|
--
|
||||||
|
-- MachineStatus predicate — the dynamic "is the machine running?" status
|
||||||
|
-- derived from the raw Source value. Boolean: true when Source > 0. Matches
|
||||||
|
-- the shape Aveva operators typically want on a machine-status tile (green
|
||||||
|
-- when above zero, otherwise grey) without needing a separate threshold
|
||||||
|
-- attribute. Rename + re-threshold here to mirror site semantics.
|
||||||
INSERT dbo.Script(GenerationId, ScriptId, Name, SourceCode, SourceHash, Language)
|
INSERT dbo.Script(GenerationId, ScriptId, Name, SourceCode, SourceHash, Language)
|
||||||
VALUES
|
VALUES
|
||||||
(@Gen, @VtScript, 'doubled-source',
|
(@Gen, @VtScript, 'machine-status-predicate',
|
||||||
N'return ((double)ctx.GetTag("/lab-floor/galaxy-line/reactor-1/Source").Value) * 2.0;',
|
N'return System.Convert.ToInt32(ctx.GetTag("/lab-floor/galaxy-line/reactor-1/Source").Value) > 0;',
|
||||||
'0000000000000000000000000000000000000000000000000000000000000000', 'CSharp'),
|
'0000000000000000000000000000000000000000000000000000000000000000', 'CSharp'),
|
||||||
(@Gen, @AlScript, 'overtemp-predicate',
|
(@Gen, @AlScript, 'overtemp-predicate',
|
||||||
N'return ((double)ctx.GetTag("/lab-floor/galaxy-line/reactor-1/Source").Value) > 50.0;',
|
N'return System.Convert.ToInt32(ctx.GetTag("/lab-floor/galaxy-line/reactor-1/Source").Value) > 50;',
|
||||||
'0000000000000000000000000000000000000000000000000000000000000000', 'CSharp');
|
'0000000000000000000000000000000000000000000000000000000000000000', 'CSharp');
|
||||||
|
|
||||||
-- 8. VirtualTag — derived value computed by Roslyn each time Source changes.
|
-- 8. VirtualTag — MachineStatus boolean computed by Roslyn each time Source
|
||||||
|
-- changes. Historized so the dashboard can plot a running/idle timeline
|
||||||
|
-- next to the raw TestHistoryValue trend from Aveva Historian.
|
||||||
INSERT dbo.VirtualTag(GenerationId, VirtualTagId, EquipmentId, Name, DataType,
|
INSERT dbo.VirtualTag(GenerationId, VirtualTagId, EquipmentId, Name, DataType,
|
||||||
ScriptId, ChangeTriggered, TimerIntervalMs, Historize, Enabled)
|
ScriptId, ChangeTriggered, TimerIntervalMs, Historize, Enabled)
|
||||||
VALUES (@Gen, @VtId, @EqId, 'Doubled', 'Float64', @VtScript, 1, NULL, 0, 1);
|
VALUES (@Gen, @VtId, @EqId, 'MachineStatus', 'Boolean', @VtScript, 1, NULL, 1, 1);
|
||||||
|
|
||||||
-- 9. ScriptedAlarm — Active when Source > 50.
|
-- 9. ScriptedAlarm — Active when Source > 50.
|
||||||
INSERT dbo.ScriptedAlarm(GenerationId, ScriptedAlarmId, EquipmentId, Name, AlarmType,
|
INSERT dbo.ScriptedAlarm(GenerationId, ScriptedAlarmId, EquipmentId, Name, AlarmType,
|
||||||
|
|||||||
Reference in New Issue
Block a user