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).
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 throughPOST /managementto theManagementActor. Role enforcement and LDAP authentication are applied there. TheManagementCommandRegistryin Commons maps command types to their names; both sides must stay in sync. - Traefik Proxy (#20) — the recommended connection target. Pointing
--urlat the Traefik address ensures requests reach the active central node without per-command failover logic in the CLI. Thedebug streamcommand's WebSocket connection (SignalR/hubs/debug-stream) also traverses Traefik, which proxies the WebSocket upgrade natively. - Audit Log (#23) — the
auditcommand group targets theGET /api/audit/queryandGET /api/audit/exportREST endpoints exposed by the Audit Log component, bypassing the management command envelope. Theaudit-configgroup (formerlyaudit-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
ManagementCommandRegistrythat maps each type to its wire name. The CLI project references Commons for these contracts. - Transport (#24) — the
bundlecommand group drives the Transport feature:bundle exportrequests a base64-encoded bundle from the server and streams it to a local.scadabundlefile;bundle previewuploads a file and returns the diff manifest;bundle importuploads 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.