chore(docker): seed script for a deployable live-data probe instance on the rig

Deferred-work register row 48. The rig had no deployable instance — all seven
soak-* rows fail pre-deployment validation (their bound data connections no longer
exist on the sites) — so debug snapshot returned instanceNotFound, no gRPC site
stream was ever opened, and the R2 batching histogram
scadabridge.site.stream.batch_size had nothing to record. Any live probe of the
data or streaming path was unverifiable.

seed-live-probe.sh rebuilds the minimum working set via the CLI only (no DB edits):
RigProbe template (Spike <- ns=3;s=SpikeData, RandomInt <- ns=3;s=RandomSignedInt32,
both ~1 Hz; Heartbeat static attribute + a trust-clean 5s interval script) and a
deployed rig-probe-1 on site-a against connection 1.

Records the two traps that cost time here:
- The custom tags in infra/opcua/nodes.json (Motor.*, Tank.*, Valve.*,
  TestChildObject.*) are DECLARED but not simulated by opc-plc — they read 0/false/
  epoch forever, so binding to them yields a Connected, Good-quality connection with
  zero event traffic. Only the built-in nodes move.
- scadabridge_site_connection_up counts open central->site gRPC SUBSCRIBER STREAMS,
  not OPC UA data connections. It reads 0 on a healthy site with no subscriber, which
  is how row 48 read it as a data-connection fault. Data-connection health lives in
  the health report's dataConnectionEndpoints / dataConnectionTagQuality.
This commit is contained in:
Joseph Doherty
2026-08-15 11:46:57 -04:00
parent 09108018f1
commit b5e6821caa
2 changed files with 156 additions and 0 deletions
+36
View File
@@ -87,6 +87,7 @@ docker/
├── build.sh # Build Docker image
├── deploy.sh # Build + deploy all containers
├── seed-sites.sh # Create test sites with Akka + gRPC addresses
├── seed-live-probe.sh # Seed one deployable live-data instance on site-a (stream traffic)
├── teardown.sh # Stop and remove containers
├── central-node-a/
│ ├── appsettings.Central.json # Central node A configuration
@@ -160,6 +161,41 @@ docker/deploy.sh
# 3. Seed test sites (first-time only, after cluster is healthy)
docker/seed-sites.sh
# 4. Seed one deployable live-data instance on site-a, so the rig has real
# stream traffic for live probes (see "Live-data probe instance" below)
docker/seed-live-probe.sh
```
### Live-data probe instance
`seed-live-probe.sh` creates the minimum working set the rig needs for any live probe
of the data or streaming path: a `RigProbe` template with two attributes bound to
**self-changing** opc-plc nodes (`ns=3;s=SpikeData`, `ns=3;s=RandomSignedInt32`, both
~1 Hz) plus a 5 s heartbeat script, and a deployed `rig-probe-1` instance on site-a.
Two traps it exists to avoid:
- **The custom tags in `infra/opcua/nodes.json` are declared but not simulated.**
`Motor.*`, `Tank.*`, `Valve.*`, `TestChildObject.*` read 0 / false / epoch forever.
Binding to them gives a `Connected` data connection with `Good` quality and *zero*
event traffic, which looks healthy and probes nothing. Use the built-in simulated
nodes (`SpikeData`, `RandomSignedInt32`, `StepUp`, `FastUInt1..5`, `SlowUInt1..5`).
- **`scadabridge_site_connection_up` does not mean what its name suggests.** It counts
open central→site gRPC *subscriber streams*, not OPC UA data connections, so it sits
at 0 on a perfectly healthy site with nobody watching. For data-connection health read
`dataConnectionEndpoints` / `dataConnectionTagQuality` off `scadabridge health site`.
Verify (metrics are on container port 8084; the containers ship no curl/wget):
```bash
scadabridge debug snapshot --id <instance-id>
scadabridge health site --identifier site-a
scadabridge debug stream --id <instance-id> # open a stream, then in another shell:
docker exec scadabridge-site-a-a bash -c \
'exec 3<>/dev/tcp/localhost/8084 && printf "GET /metrics HTTP/1.0\r\nHost: localhost\r\n\r\n" >&3 && cat <&3' \
| grep -E 'site_connection_up|stream_batch_size'
```
### After Code Changes
+120
View File
@@ -0,0 +1,120 @@
#!/bin/bash
set -euo pipefail
# Seed ONE deployable, live-data instance on site-a so the rig always has real
# stream traffic for live probes.
#
# Why this exists: the rig repeatedly ends up with instances that cannot deploy
# (deferred-work register row 48 — the seven `soak-*` rows fail pre-deployment
# validation because their bound data connections no longer exist on the sites).
# With nothing deployed, `debug snapshot` returns `instanceNotFound`, no gRPC site
# stream is ever opened, and the R2 batching histogram
# `scadabridge.site.stream.batch_size` has nothing to record — so any live probe of
# the streaming path is unverifiable. This script rebuilds the minimum working set
# from scratch, using only the CLI (no DB edits).
#
# What it creates (all on site-a / site id 1, against data connection id 1
# "OPC PLC Simulator" -> opc.tcp://scadabridge-opcua:50000):
# - Template `RigProbe`
# * Spike (Double) <- ns=3;s=SpikeData — opc-plc built-in, changes ~1/s
# * RandomInt (Int32) <- ns=3;s=RandomSignedInt32 — opc-plc built-in, changes ~1/s
# * Heartbeat (Int32) — static, bumped every 5s by the Heartbeat script
# * Heartbeat script — Interval 5000ms, trust-policy-clean
# - Instance `rig-probe-1`, bound + deployed
#
# IMPORTANT — pick self-changing tags. The custom tags in `infra/opcua/nodes.json`
# (Motor.*, Tank.*, Valve.*, TestChildObject.*) are declared but NOT simulated by
# opc-plc: they read 0 / false / epoch and never change on their own, so binding to
# them yields a healthy connection with zero event traffic. The built-in simulated
# nodes are the ones that move: SpikeData, RandomSignedInt32, StepUp, FastUInt1..5
# (1s) and SlowUInt1..5 (10s).
#
# Prerequisites:
# - Infrastructure services running (infra/docker-compose.yml — needs scadabridge-opcua)
# - Application containers running (docker/deploy.sh)
# - Sites seeded (docker/seed-sites.sh) and central healthy
# curl -sf http://localhost:9000/health/ready
#
# Usage:
# docker/seed-live-probe.sh
#
# Idempotency: this script is NOT idempotent — template and instance names must be
# unique, so a second run fails on the create steps. To re-seed, delete the previous
# instance and template first:
# scadabridge instance delete --id <id> && scadabridge template delete --id <id>
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
CLI="dotnet run --project $PROJECT_ROOT/src/ZB.MOM.WW.ScadaBridge.CLI --"
AUTH="--username multi-role --password password"
URL="--url http://localhost:9000"
SITE_ID=1 # site-a
CONNECTION_ID=1 # "OPC PLC Simulator" on site-a
TEMPLATE_NAME="RigProbe"
INSTANCE_NAME="rig-probe-1"
# Pulls the numeric "id" out of the CLI's JSON reply without needing jq.
json_id() { python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])'; }
echo "=== Seeding live-data probe instance on site-a ==="
echo ""
echo "1/6 Creating template $TEMPLATE_NAME..."
TEMPLATE_ID=$($CLI $URL $AUTH template create \
--name "$TEMPLATE_NAME" \
--description "Minimal live-traffic probe: two OPC UA sim tags that change on their own + a 5s heartbeat script" \
| json_id)
echo " template id = $TEMPLATE_ID"
echo ""
echo "2/6 Adding data-bound attributes..."
$CLI $URL $AUTH template attribute add \
--template-id "$TEMPLATE_ID" --name Spike --data-type Double \
--data-source "ns=3;s=SpikeData" \
--description "opc-plc SpikeData — Double, changes every ~1s on its own" > /dev/null
$CLI $URL $AUTH template attribute add \
--template-id "$TEMPLATE_ID" --name RandomInt --data-type Int32 \
--data-source "ns=3;s=RandomSignedInt32" \
--description "opc-plc RandomSignedInt32 — Int32, changes every ~1s on its own" > /dev/null
echo ""
echo "3/6 Adding the static heartbeat attribute + its interval script..."
$CLI $URL $AUTH template attribute add \
--template-id "$TEMPLATE_ID" --name Heartbeat --data-type Int32 --value 0 \
--description "Static attribute bumped every 5s by the Heartbeat script — script-sourced traffic" > /dev/null
$CLI $URL $AUTH template script add \
--template-id "$TEMPLATE_ID" --name Heartbeat \
--trigger-type Interval --trigger-config '{"intervalMs":5000}' \
--code 'var current = await Instance.GetAttribute("Heartbeat"); var next = (int.TryParse(current?.ToString(), out var n) ? n : 0) + 1; await Instance.SetAttribute("Heartbeat", next.ToString());' > /dev/null
echo ""
echo "4/6 Creating instance $INSTANCE_NAME..."
INSTANCE_ID=$($CLI $URL $AUTH instance create \
--name "$INSTANCE_NAME" --template-id "$TEMPLATE_ID" --site-id "$SITE_ID" | json_id)
echo " instance id = $INSTANCE_ID"
echo ""
echo "5/6 Binding the data-sourced attributes to connection $CONNECTION_ID..."
$CLI $URL $AUTH instance set-bindings --id "$INSTANCE_ID" \
--bindings "[[\"Spike\",$CONNECTION_ID],[\"RandomInt\",$CONNECTION_ID]]" > /dev/null
echo ""
echo "6/6 Deploying..."
$CLI $URL $AUTH instance deploy --id "$INSTANCE_ID"
echo ""
echo "=== Done. Verify with: ==="
echo " scadabridge debug snapshot --id $INSTANCE_ID"
echo " scadabridge health site --identifier site-a # dataConnectionEndpoints / dataConnectionTagQuality"
echo " scadabridge debug stream --id $INSTANCE_ID # opens a gRPC site stream; ~2 events/s"
echo ""
echo "While a stream is open, the site node serving it reports both signals"
echo "(metrics are on container port 8084, and the containers have no curl/wget):"
echo " docker exec scadabridge-site-a-a bash -c 'exec 3<>/dev/tcp/localhost/8084 && printf \"GET /metrics HTTP/1.0\\r\\nHost: localhost\\r\\n\\r\\n\" >&3 && cat <&3' \\"
echo " | grep -E 'site_connection_up|stream_batch_size'"
echo ""
echo "NOTE: scadabridge_site_connection_up counts OPEN CENTRAL->SITE gRPC SUBSCRIBER"
echo "STREAMS, not OPC UA data connections. It reads 0 on a perfectly healthy site"
echo "with no subscriber attached — use the health report's dataConnectionEndpoints"
echo "for data-connection health."