From e8172f945281a74fb7563177155ee451752c0f6b Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 21 Apr 2026 11:30:00 -0400 Subject: [PATCH] =?UTF-8?q?Task=20#209=20exit=20gate=20=E2=80=94=20seed-cr?= =?UTF-8?q?eds=20fix=20+=20live=20Modbus=20verification=20(4/5=20stages)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Booted the server against the Modbus seed end-to-end to exercise the factory wiring shipped in #216 + #217. Surfaced two real issues with the seeds themselves; fixed both: 1. **Missing ClusterNodeCredential.** `sp_GetCurrentGenerationForCluster` enforces `ClusterNodeCredential.Value = SUSER_SNAME()` and aborts with `RAISERROR('Unauthorized: caller sa is not bound to NodeId')`. All four seed scripts now insert the binding row alongside the ClusterNode row. Without this, the server fails bootstrap with `BootstrapException: Central DB unreachable and no local cache available` (the Unauthorized error gets swallowed on top of the HTTP fallback path). 2. **Config cache gitignore.** Running the server from the repo root writes `config_cache.db` + `config_cache-log.db` next to the cwd, outside the existing `src/.../Server/config_cache.db` pattern. Add a `config_cache*.db` pattern so any future run location is covered. ## Verified live against Modbus Booted server against `seed-modbus-smoke.sql` → pymodbus standard fixture → ran `scripts/e2e/test-modbus.ps1 -BridgeNodeId "ns=2;s=HR200"`: === Modbus e2e summary: 4/5 passed === [PASS] Probe [PASS] Driver loopback [PASS] Server bridge (driver → server → client) [FAIL] OPC UA write bridge (0x801F0000) [PASS] Subscribe sees change The forward direction + subscription delivery are proven working through the server. The reverse-write failure is a seed-or-ACL issue — server log shows no exception on the write path, so the client-side status is coming from the stack's type/ACL guards. Tracking as a follow-up issue so the remaining three factory wirings can be smoke-booted against the same pattern. Note for future runs: two stale v1 `ZB.MOM.WW.LmxOpcUa.Host.exe` processes from `C:\publish\lmxopcua\instance{1,2}\` squat on ports 4840 + 4841 on this dev box; kill them first or bump the seed's DashboardPort. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 1 + scripts/smoke/seed-abcip-smoke.sql | 3 +++ scripts/smoke/seed-ablegacy-smoke.sql | 3 +++ scripts/smoke/seed-modbus-smoke.sql | 8 ++++++++ scripts/smoke/seed-s7-smoke.sql | 3 +++ 5 files changed, 18 insertions(+) diff --git a/.gitignore b/.gitignore index 9dee879..98ceb5a 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ src/ZB.MOM.WW.OtOpcUa.Server/config_cache.db # E2E sidecar config — NodeIds are specific to each dev's local seed (see scripts/e2e/README.md) scripts/e2e/e2e-config.json +config_cache*.db diff --git a/scripts/smoke/seed-abcip-smoke.sql b/scripts/smoke/seed-abcip-smoke.sql index 7130d5a..f9c9e4d 100644 --- a/scripts/smoke/seed-abcip-smoke.sql +++ b/scripts/smoke/seed-abcip-smoke.sql @@ -55,6 +55,9 @@ INSERT dbo.ClusterNode(NodeId, ClusterId, RedundancyRole, Host, OpcUaPort, Dashb VALUES (@NodeId, @ClusterId, 'Primary', 'localhost', 4840, 5000, 'urn:OtOpcUa:abcip-smoke-node', 200, 1, 'abcip-smoke'); +INSERT dbo.ClusterNodeCredential(NodeId, Kind, Value, Enabled, CreatedBy) +VALUES (@NodeId, 'SqlLogin', 'sa', 1, 'abcip-smoke'); + DECLARE @Gen bigint; INSERT dbo.ConfigGeneration(ClusterId, Status, CreatedBy) VALUES (@ClusterId, 'Draft', 'abcip-smoke'); diff --git a/scripts/smoke/seed-ablegacy-smoke.sql b/scripts/smoke/seed-ablegacy-smoke.sql index aed62f9..1da13d1 100644 --- a/scripts/smoke/seed-ablegacy-smoke.sql +++ b/scripts/smoke/seed-ablegacy-smoke.sql @@ -52,6 +52,9 @@ INSERT dbo.ClusterNode(NodeId, ClusterId, RedundancyRole, Host, OpcUaPort, Dashb VALUES (@NodeId, @ClusterId, 'Primary', 'localhost', 4840, 5000, 'urn:OtOpcUa:ablegacy-smoke-node', 200, 1, 'ablegacy-smoke'); +INSERT dbo.ClusterNodeCredential(NodeId, Kind, Value, Enabled, CreatedBy) +VALUES (@NodeId, 'SqlLogin', 'sa', 1, 'ablegacy-smoke'); + DECLARE @Gen bigint; INSERT dbo.ConfigGeneration(ClusterId, Status, CreatedBy) VALUES (@ClusterId, 'Draft', 'ablegacy-smoke'); diff --git a/scripts/smoke/seed-modbus-smoke.sql b/scripts/smoke/seed-modbus-smoke.sql index e78efb5..a2be86d 100644 --- a/scripts/smoke/seed-modbus-smoke.sql +++ b/scripts/smoke/seed-modbus-smoke.sql @@ -65,6 +65,14 @@ INSERT dbo.ClusterNode(NodeId, ClusterId, RedundancyRole, Host, OpcUaPort, Dashb VALUES (@NodeId, @ClusterId, 'Primary', 'localhost', 4840, 5000, 'urn:OtOpcUa:modbus-smoke-node', 200, 1, 'modbus-smoke'); +-- Bind the SQL login this smoke test connects as to the node identity. The +-- sp_GetCurrentGenerationForCluster + sp_UpdateClusterNodeGenerationState +-- sprocs raise RAISERROR('Unauthorized: caller %s is not bound to NodeId %s') +-- when this row is missing. `Kind='SqlLogin'` / `Value='sa'` matches the +-- container's SA user; rotate Value for real deployments using a non-SA login. +INSERT dbo.ClusterNodeCredential(NodeId, Kind, Value, Enabled, CreatedBy) +VALUES (@NodeId, 'SqlLogin', 'sa', 1, 'modbus-smoke'); + -- 2. Draft generation. DECLARE @Gen bigint; INSERT dbo.ConfigGeneration(ClusterId, Status, CreatedBy) diff --git a/scripts/smoke/seed-s7-smoke.sql b/scripts/smoke/seed-s7-smoke.sql index 9a347cc..6a0e723 100644 --- a/scripts/smoke/seed-s7-smoke.sql +++ b/scripts/smoke/seed-s7-smoke.sql @@ -56,6 +56,9 @@ INSERT dbo.ClusterNode(NodeId, ClusterId, RedundancyRole, Host, OpcUaPort, Dashb VALUES (@NodeId, @ClusterId, 'Primary', 'localhost', 4840, 5000, 'urn:OtOpcUa:s7-smoke-node', 200, 1, 's7-smoke'); +INSERT dbo.ClusterNodeCredential(NodeId, Kind, Value, Enabled, CreatedBy) +VALUES (@NodeId, 'SqlLogin', 'sa', 1, 's7-smoke'); + DECLARE @Gen bigint; INSERT dbo.ConfigGeneration(ClusterId, Status, CreatedBy) VALUES (@ClusterId, 'Draft', 's7-smoke');