66f0f96328
- Fix 15 link-text/target mismatches (ConfigurationDatabase ×8 to Commons, NotificationOutbox ×4, ClusterInfrastructure case, HealthMonitoring, SiteCallAudit) caught by a link-text-vs-target consistency check. - Tag 14 untagged code-fence openers (ASCII diagrams/trees, JSON, HTTP). - Correct 4 type names to match source (ValidationService, HealthReportSender, CentralCommunicationActor, DebugSnapshotCommand set). - Soften Traefik version prose per the style guide.
251 lines
16 KiB
Markdown
251 lines
16 KiB
Markdown
# 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`. JSON output goes to stdout and is formatted with indented, camelCase JSON. 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:
|
|
|
|
```csharp
|
|
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
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```bash
|
|
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`, `deploy`, `enable`, `disable`, `delete`, `set-bindings`; `native-alarm-source set/clear` | `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`; `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
|
|
|
|
```bash
|
|
# 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.
|
|
|
|
```json
|
|
{
|
|
"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)](./ManagementService.md) — 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)](./TraefikProxy.md) — 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)](./AuditLog.md) — 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)](./Security.md) — 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)](./Commons.md) — 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)](./Transport.md) — 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](../requirements/Component-CLI.md).
|
|
|
|
## Troubleshooting
|
|
|
|
### No management URL
|
|
|
|
```json
|
|
{"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
|
|
|
|
```json
|
|
{"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
|
|
|
|
```text
|
|
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
|
|
|
|
```text
|
|
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` and `bundle import` use a 5-minute per-command timeout (compared to the 30-second default). If a bundle operation times out, the server-side export or import may still be running. Re-try with a smaller selection or check the central node logs.
|
|
|
|
## Related Documentation
|
|
|
|
- [CLI design specification](../requirements/Component-CLI.md)
|
|
- [Management Service](./ManagementService.md)
|
|
- [Traefik Proxy](./TraefikProxy.md)
|
|
- [Audit Log](./AuditLog.md)
|
|
- [Security](./Security.md)
|
|
- [Commons](./Commons.md)
|