Files
Joseph Doherty 0c3837c778 docs(components): accuracy fixes from deep review (batch 4)
ManagementService (role table: queries any-auth, area mutations Designer;
audit contract exception), CLI (missing instance/api-key subcommands; server
JSON printed verbatim; bundle preview timeout), Transport (BundleFormatVersion
exact-match gate; dependency scan fields; three flushes), CentralUI
(/api/script-analysis endpoints; LoginLayout minimal; Health tile components),
TreeView (Topology no RevealNode; ContextMenu Site branch; InitiallyExpanded).
2026-06-03 16:39:29 -04:00

17 KiB

CLI

The CLI is a standalone command-line tool that provides scripting and automation access to all ScadaBridge administrative operations. It connects to the central cluster's HTTP Management API and dispatches commands to the ManagementActor via POST /management. Authentication and role resolution happen server-side; the CLI sends LDAP credentials as HTTP Basic Auth on every request.

Overview

The CLI component lives in src/ZB.MOM.WW.ScadaBridge.CLI/ and builds to a self-contained scadabridge executable (or scadabridge.exe on Windows). It is not part of the Host binary — it deploys on any machine with HTTP access to a central node.

The tool is built on System.CommandLine and exposes a hierarchical command tree organized by management domain (template, instance, site, data-connection, and so on). Every command follows the same pattern: resolve the management URL and credentials, construct a command object whose type maps to a ManagementCommandRegistry entry, serialize it into the { command, payload } JSON envelope, and POST it to <managementUrl>/management. The server response — JSON success body or { error, code } error envelope — is printed to stdout or stderr respectively.

The CLI is the preferred automation interface. When setting up system state (sites, templates, data connections, deployments, security) without the Central UI, use scadabridge commands rather than direct database manipulation.

Key Concepts

Connection to the active node

The CLI connects through Traefik, the reverse proxy fronting the central cluster. Traefik routes each request to the active central node based on the /health/active probe, so the CLI does not need to know which node (central-a or central-b) is currently primary. Pointing --url (or managementUrl) at the Traefik address (default http://localhost:9000 in Docker) provides automatic HA failover without any CLI-side configuration change.

Direct-to-node access (http://localhost:9001 for central-a, http://localhost:9002 for central-b) bypasses Traefik and is useful for diagnostics but not for production automation.

Command dispatch

Most commands serialize to a named management command and reach the ManagementActor through the POST /management endpoint. The audit query and audit export commands are the exception — they call plain REST endpoints (GET /api/audit/query, GET /api/audit/export) introduced by Audit Log (#23) and therefore use SendGetAsync/SendGetStreamAsync on ManagementHttpClient rather than SendCommandAsync. The streaming audit export path uses HttpCompletionOption.ResponseHeadersRead to avoid buffering large payloads in memory.

Authentication and roles

The CLI encodes --username and --password as an HTTP Basic Auth header on every request. The server performs the LDAP bind, group lookup, and role resolution. The CLI never contacts LDAP directly and never caches credentials between invocations.

Role enforcement is applied by the ManagementActor. Operations require the appropriate role:

Role Covers
Admin Security settings, site management, SMTP config, audit-config queries
Design Templates, shared scripts, external systems, DB connections, API methods, notification lists
Deployment Instance lifecycle (deploy, enable, disable, delete), data connection bindings, bundle import/export
OperationalAudit Reading the Audit Log (audit query)
AuditExport Exporting the Audit Log (audit export)

A request lacking the required role exits with code 2 (authorization failure). A bad-credential response (HTTP 401) exits with code 1.

Output formats

Every command accepts --format json (default) or --format table. For JSON output, successful server responses are written to stdout verbatim — the server controls the JSON shape and no re-serialization is applied. Locally-constructed output (errors, debug stream events) is serialized by OutputFormatter with indentation and camelCase. Table output renders a padded plain-text table derived from the response JSON — arrays become rows, single objects become a two-column Property / Value table. Errors are always written as { "error": "...", "code": "..." } to stderr, regardless of format.

Architecture

Program.cs builds the RootCommand tree, attaches the four global options (--url, --username, --password, --format), and registers each command group:

rootCommand.Add(TemplateCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(InstanceCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(SiteCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(DeployCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(DataConnectionCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(ExternalSystemCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(NotificationCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(SecurityCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(AuditLogCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(AuditCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(HealthCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(DebugCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(SharedScriptCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(DbConnectionCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(ApiMethodCommands.Build(urlOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(BundleCommands.Build(urlOption, formatOption, usernameOption, passwordOption));

Every command's action delegates to CommandHelpers.ExecuteCommandAsync, which owns the URL/credential resolution, ManagementHttpClient lifetime, and exit-code mapping. This consolidation means the authorization-failure exit code (2) is enforced uniformly — including for the bundle group, which uses ExecuteCommandAsync with a longer 5-minute timeout and a per-command onSuccess handler rather than a separate HTTP path.

CliConfig.Load() is called at the start of every invocation. It merges ~/.scadabridge/config.json, environment variables, and any defaults. A malformed or unreadable config file emits a warning to stderr and falls through to the environment variable / command-line precedence chain without crashing.

Usage

Running the CLI

# Build
dotnet build src/ZB.MOM.WW.ScadaBridge.CLI

# Minimal invocation (URL and credentials required)
scadabridge --url http://localhost:9000 --username multi-role --password password template list

# Using Traefik (HA; routes to the active node automatically)
scadabridge --url http://localhost:9000 --username multi-role --password password site list

# Table output
scadabridge --url http://localhost:9000 --username multi-role --password password \
  --format table instance list --site-id 1

Credentials are safer supplied via environment variables than on the command line, where they appear in process listings and shell history:

export SCADABRIDGE_MANAGEMENT_URL=http://localhost:9000
export SCADABRIDGE_USERNAME=multi-role
export SCADABRIDGE_PASSWORD=password

scadabridge template create --name "PumpStation" --description "Standard pump station"
scadabridge template attribute add --template-id 3 --name Speed --data-type Float --default-value 0
scadabridge instance create --name "PS-01" --template-id 3 --site-id 1
scadabridge instance deploy --id 7

Command groups

Group Subcommands Role required
template list, get, create, update, delete, validate; attribute add/update/delete; alarm add/update/delete; script add/update/delete; composition add/delete; native-alarm-source add/list/remove Design
instance list, get, create, set-bindings, set-overrides, alarm-override set/delete/list, native-alarm-source set/clear, set-area, diff, deploy, enable, disable, delete Deployment
site list, get, create, delete, deploy-artifacts; area list/create/update/delete Admin
deploy instance, artifacts, status Deployment
data-connection list, get, create, update, delete Design / Deployment
external-system list, get, create, update, delete Design
notification list, get, create, update, delete; smtp list/update Design / Admin
security api-key list/create/update/delete/set-methods; role-mapping list/create/update/delete; scope-rule list/add/delete Admin
shared-script list, get, create, update, delete Design
db-connection list, get, create, update, delete Design
api-method list, get, create, update, delete Design
bundle export, preview, import Deployment
audit query, export, verify-chain OperationalAudit / AuditExport
audit-config query (config-change audit trail; was audit-log pre-M8) Admin
health summary, site, event-log, parked-messages Deployment
debug snapshot, stream Deployment

Selected examples

# Query the operational Audit Log for failed API outbound events in the last 24 hours
scadabridge audit query --since 24h --channel ApiOutbound --errors-only --format table

# Export a full audit window to CSV
scadabridge audit export --since 2026-05-01T00:00:00Z --until 2026-06-01T00:00:00Z \
  --format csv --output /tmp/audit-may-2026.csv

# Export a Transport bundle for selected templates (with transitive dependencies)
scadabridge bundle export --output /tmp/pump-station.scadabundle \
  --templates "PumpStation,BaseModule" --include-dependencies

# Preview a bundle diff before importing
scadabridge bundle preview --input /tmp/pump-station.scadabundle

# Import with overwrite conflict policy
scadabridge bundle import --input /tmp/pump-station.scadabundle --on-conflict overwrite

# Stream live attribute and alarm changes for a running instance
scadabridge debug stream --id 7

# Query deployment records for a specific instance
scadabridge deploy status --instance-id 7 --page-size 20

Exit codes

Code Meaning
0 Success
1 Command error (connection failure, validation error, server error)
2 Authorization failure (insufficient role; HTTP 403 or FORBIDDEN/UNAUTHORIZED error code)

Configuration

~/.scadabridge/config.json is loaded on every invocation. A malformed or unreadable file emits a warning to stderr; it does not abort the invocation.

{
  "managementUrl": "http://localhost:9000",
  "defaultFormat": "json"
}
Key Default Description
managementUrl Base URL for the Management API. Overridden by SCADABRIDGE_MANAGEMENT_URL env var, then by --url.
defaultFormat json Default output format when --format is not supplied. Overridden by SCADABRIDGE_FORMAT env var, then by --format.

Credentials are intentionally never stored in the config file — they are sourced from environment variables or supplied per-invocation on the command line. Storing them in the file would persist them to disk in plaintext.

Environment variables

Variable Overrides Description
SCADABRIDGE_MANAGEMENT_URL managementUrl in config file Management API base URL
SCADABRIDGE_FORMAT defaultFormat in config file Default output format
SCADABRIDGE_USERNAME LDAP username; overridden by --username
SCADABRIDGE_PASSWORD LDAP password; overridden by --password. Preferred over --password to avoid leaking credentials into process listings and shell history.

URL precedence

The management URL is resolved in this order: --url flag → SCADABRIDGE_MANAGEMENT_URL env var → managementUrl in config file. If none is set, the command exits with code 1 and a NO_URL error.

Dependencies & Interactions

  • Management Service (#18) — the server-side counterpart. Every CLI command (except audit query/audit export) translates to a named management command dispatched through POST /management to the ManagementActor. Role enforcement and LDAP authentication are applied there. The ManagementCommandRegistry in Commons maps command types to their names; both sides must stay in sync.
  • Traefik Proxy (#20) — the recommended connection target. Pointing --url at the Traefik address ensures requests reach the active central node without per-command failover logic in the CLI. The debug stream command's WebSocket connection (SignalR /hubs/debug-stream) also traverses Traefik, which proxies the WebSocket upgrade natively.
  • Audit Log (#23) — the audit command group targets the GET /api/audit/query and GET /api/audit/export REST endpoints exposed by the Audit Log component, bypassing the management command envelope. The audit-config group (formerly audit-log) targets the configuration-change audit trail (IAuditService) via the standard management envelope.
  • Security & Auth (#10) — the server resolves LDAP credentials and maps group memberships to ScadaBridge roles (Admin, Design, Deployment, OperationalAudit, AuditExport). The CLI does not interact with LDAP directly.
  • Commons (#16) — owns the management command record types and the ManagementCommandRegistry that maps each type to its wire name. The CLI project references Commons for these contracts.
  • Transport (#24) — the bundle command group drives the Transport feature: bundle export requests a base64-encoded bundle from the server and streams it to a local .scadabundle file; bundle preview uploads a file and returns the diff manifest; bundle import uploads a file and applies it with a configurable conflict policy.
  • Design spec: Component-CLI.md.

Troubleshooting

No management URL

{"error":"No management URL specified. Use --url, set SCADABRIDGE_MANAGEMENT_URL, or add 'managementUrl' to ~/.scadabridge/config.json.","code":"NO_URL"}

The URL is not set via --url, SCADABRIDGE_MANAGEMENT_URL, or the config file. Set one of these before running any command.

Connection failed

{"error":"Connection failed: Connection refused (localhost:9000)","code":"CONNECTION_FAILED"}

The central node or Traefik is not reachable at the configured URL. Verify the cluster is running and the URL matches the Traefik port (default 9000 in Docker) or the node's direct port (9001/9002).

Authorization failure (exit 2)

The server returned HTTP 403 or an error code of FORBIDDEN/UNAUTHORIZED. The authenticated user's LDAP groups do not map to a role with permission for the requested operation. Use security role-mapping list (requires Admin) to inspect role mappings. The multi-role test user (password) holds Admin, Design, and Deployment roles.

Malformed config file warning

warning: ignoring malformed or unreadable /home/user/.scadabridge/config.json: ...

CliConfig.Load() caught a JsonException, IOException, or UnauthorizedAccessException reading the config file. The invocation continues using environment variables and command-line options. Fix or recreate the config file.

audit-log deprecation warning

Warning: 'audit-log' is deprecated and will be removed in a future release. Use 'audit-config' instead.

The audit-log command group was renamed to audit-config in M8 of Audit Log (#23). The old name still works but emits this warning to stderr. Migrate any scripts from scadabridge audit-log ... to scadabridge audit-config ....

Bundle timeout

bundle export, bundle preview, and bundle import all use a 5-minute per-command timeout (compared to the 30-second default). If a bundle operation times out, the server-side operation may still be running. Re-try with a smaller selection or check the central node logs.