feat: add HTTP Management API, migrate CLI from Akka ClusterClient to HTTP

Replace the CLI's Akka.NET ClusterClient transport with a simple HTTP client
targeting a new POST /management endpoint on the Central Host. The endpoint
handles Basic Auth, LDAP authentication, role resolution, and ManagementActor
dispatch in a single round-trip — eliminating the CLI's Akka, LDAP, and
Security dependencies.

Also fixes DCL ReSubscribeAll losing subscriptions on repeated reconnect by
deriving the tag list from _subscriptionsByInstance instead of _subscriptionIds.
This commit is contained in:
Joseph Doherty
2026-03-20 23:55:31 -04:00
parent 7740a3bcf9
commit 1a540f4f0a
38 changed files with 863 additions and 758 deletions

View File

@@ -152,9 +152,9 @@ This project contains design documentation for a distributed SCADA system built
### CLI Quick Reference (Docker / OrbStack)
- **Contact point**: `akka.tcp://scadalink@scadalink-central-a:8081` — the hostname must match the container's Akka `NodeHostname` config. Do NOT use `localhost:9011`; Akka remoting requires the hostname in the URI to match what the node advertises.
- **Management URL**: `http://localhost:9001` — the CLI connects to the Central Host's HTTP management API (port 5000 mapped to 9001 in Docker).
- **Test user**: `--username multi-role --password password` — has Admin, Design, and Deployment roles. The `admin` user only has the Admin role and cannot create templates, data connections, or deploy.
- **Config file**: `~/.scadalink/config.json` — stores contact points, LDAP settings (including `searchBase`, `serviceAccountDn`, `serviceAccountPassword`), and default format. See `docker/README.md` for a ready-to-use test config.
- **Config file**: `~/.scadalink/config.json` — stores `managementUrl` and default format. See `docker/README.md` for a ready-to-use test config.
- **Rebuild cluster**: `bash docker/deploy.sh` — builds the `scadalink:latest` image and recreates all containers. Run this after code changes to ManagementActor, Host, or any server-side component.
- **Infrastructure services**: `cd infra && docker compose up -d` — starts LDAP, MS SQL, OPC UA, SMTP, REST API, and LmxFakeProxy. These are separate from the cluster containers in `docker/`.
- **All test LDAP passwords**: `password` (see `infra/glauth/config.toml` for users and groups).

View File

@@ -2,47 +2,43 @@
## Purpose
The CLI is a standalone command-line tool for scripting and automating administrative operations against the ScadaLink central cluster. It connects to the ManagementActor via Akka.NET ClusterClient — it does not join the cluster as a full member and does not use HTTP/REST. The CLI provides the same administrative capabilities as the Central UI, enabling automation, batch operations, and integration with CI/CD pipelines.
The CLI is a standalone command-line tool for scripting and automating administrative operations against the ScadaLink central cluster. It connects to the Central Host's HTTP Management API (`POST /management`), which dispatches commands to the ManagementActor. Authentication and role resolution are handled server-side — the CLI sends credentials via HTTP Basic Auth. The CLI provides the same administrative capabilities as the Central UI, enabling automation, batch operations, and integration with CI/CD pipelines.
## Location
Standalone executable, not part of the Host binary. Deployed on any Windows machine with network access to the central cluster.
Standalone executable, not part of the Host binary. Deployed on any machine with HTTP access to a central node.
`src/ScadaLink.CLI/`
## Responsibilities
- Parse command-line arguments and dispatch to the appropriate management operation.
- Authenticate the user via LDAP credentials and include identity in every message sent to the ManagementActor.
- Connect to the central cluster via Akka.NET ClusterClient using configured contact points.
- Send management messages to the ManagementActor and display structured responses.
- Send HTTP requests to the Central Host's Management API endpoint with Basic Auth credentials.
- Display structured responses from the Management API.
- Support both JSON and human-readable table output formats.
## Technology
- **Argument parsing**: `System.CommandLine` library for command/subcommand/option parsing with built-in help generation.
- **Transport**: Akka.NET `ClusterClient` connecting to `ClusterClientReceptionist` on the central cluster. The CLI does not join the cluster — it is a lightweight external client.
- **Serialization**: Message contracts from Commons (`Messages/Management/`), same as ManagementActor expects.
- **Transport**: HTTP client connecting to the Central Host's `POST /management` endpoint. Authentication is via HTTP Basic Auth — the server performs LDAP bind and role resolution.
- **Serialization**: Commands serialized as JSON with a type discriminator (`command` field). Message contracts from Commons define the command types.
## Authentication
The CLI authenticates the user against LDAP/AD before any operation:
The CLI sends user credentials to the Management API via HTTP Basic Auth:
1. The user provides credentials via `--username` / `--password` options, or is prompted interactively if omitted.
2. The CLI performs a direct LDAP bind against the configured LDAP server (same mechanism as the Central UI login).
3. On successful bind, the CLI queries group memberships to determine roles and permitted sites.
4. Every message sent to the ManagementActor includes the `AuthenticatedUser` envelope with the user's identity, roles, and site permissions.
5. Credentials are not stored or cached between invocations. Each CLI invocation requires fresh authentication.
LDAP connection settings are read from the CLI configuration (see Configuration section).
1. The user provides credentials via `--username` / `--password` options.
2. On each request, the CLI encodes credentials as a Basic Auth header and sends them with the command.
3. The server performs LDAP authentication, group lookup, and role resolution — the CLI does not communicate with LDAP directly.
4. Credentials are not stored or cached between invocations. Each CLI invocation requires fresh credentials.
## Connection
The CLI uses Akka.NET ClusterClient to connect to the central cluster:
The CLI connects to the Central Host via HTTP:
- **Contact points**: One or more seed node addresses for the ClusterClientReceptionist. The CLI sends an initial contact to these addresses; the receptionist responds with the current set of cluster nodes hosting the ManagementActor.
- **No cluster membership**: The CLI does not join the Akka.NET cluster. It is an external process that communicates via the ClusterClient protocol.
- **Failover**: If the active central node fails over, ClusterClient transparently reconnects to the new active node via the receptionist. In-flight commands may time out and need to be retried.
- **Management URL**: The URL of a central node's web server (e.g., `http://localhost:9001`). The management API is served at `POST /management` on the same host as the Central UI.
- **Failover**: For HA, use a load balancer URL in front of both central nodes. The management API is stateless (Basic Auth per request), so any central node can handle any request without sticky sessions.
- **No Akka.NET dependency**: The CLI is a pure HTTP client with no Akka.NET runtime.
## Command Structure
@@ -205,28 +201,17 @@ scadalink api-method delete --id <id>
Configuration is resolved in the following priority order (highest wins):
1. **Command-line options**: `--contact-points`, `--username`, `--password`, `--format`.
1. **Command-line options**: `--url`, `--username`, `--password`, `--format`.
2. **Environment variables**:
- `SCADALINK_CONTACT_POINTS` — Comma-separated list of central cluster contact point addresses (e.g., `akka.tcp://ScadaLink@central1:8081,akka.tcp://ScadaLink@central2:8081`).
- `SCADALINK_LDAP_SERVER` — LDAP server address.
- `SCADALINK_LDAP_PORT` — LDAP port (default: 636 for LDAPS).
- `SCADALINK_MANAGEMENT_URL` — Management API URL (e.g., `http://central-host:5000`).
- `SCADALINK_FORMAT` — Default output format (`json` or `table`).
3. **Configuration file**: `~/.scadalink/config.json` — Persistent defaults for contact points, LDAP settings, and output format.
3. **Configuration file**: `~/.scadalink/config.json` — Persistent defaults for management URL and output format.
### Configuration File Format
```json
{
"contactPoints": [
"akka.tcp://ScadaLink@central1:8081",
"akka.tcp://ScadaLink@central2:8081"
],
"ldap": {
"server": "ad.example.com",
"port": 636,
"useTls": true
},
"defaultFormat": "json"
"managementUrl": "http://central-host:5000"
}
```
@@ -240,28 +225,22 @@ Configuration is resolved in the following priority order (highest wins):
| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | General error (command failed) |
| 2 | Authentication failure (LDAP bind failed) |
| 3 | Authorization failure (insufficient role) |
| 4 | Connection failure (cannot reach central cluster) |
| 5 | Validation failure (e.g., template validation errors) |
| 1 | General error (command failed, connection failure, or authentication failure) |
| 2 | Authorization failure (insufficient role) |
## Error Handling
- **Connection failure**: If the CLI cannot establish a ClusterClient connection within a timeout (default 10 seconds), it exits with code 4 and a descriptive error message.
- **Command timeout**: If the ManagementActor does not respond within 30 seconds (configurable), the command fails with a timeout error.
- **Authentication failure**: If the LDAP bind fails, the CLI exits with code 2 before sending any commands.
- **Authorization failure**: If the ManagementActor returns an Unauthorized response, the CLI exits with code 3.
- **Connection failure**: If the CLI cannot connect to the management URL (e.g., DNS failure, connection refused), it exits with code 1 and a descriptive error message.
- **Command timeout**: If the server does not respond within 30 seconds, the command fails with a timeout error (HTTP 504).
- **Authentication failure**: If the server returns HTTP 401 (LDAP bind failed), the CLI exits with code 1.
- **Authorization failure**: If the server returns HTTP 403, the CLI exits with code 2.
## Dependencies
- **Commons**: Message contracts (`Messages/Management/`), shared types.
- **Commons**: Message contracts (`Messages/Management/`) for command type definitions and registry.
- **System.CommandLine**: Command-line argument parsing.
- **Akka.NET (Akka.Cluster.Tools)**: ClusterClient for communication with the central cluster.
- **LDAP client library**: For direct LDAP bind authentication (same approach as Security & Auth component).
## Interactions
- **Management Service (ManagementActor)**: The CLI's sole runtime dependency. All operations are sent as messages to the ManagementActor via ClusterClient.
- **Security & Auth**: The CLI performs LDAP authentication independently (same LDAP server, same bind mechanism) and passes the authenticated identity to the ManagementActor. The ManagementActor enforces authorization.
- **LDAP/Active Directory**: Direct bind for user authentication before any operation.
- **Management Service (via HTTP)**: The CLI's sole runtime dependency. All operations are sent as HTTP POST requests to the Management API endpoint on a central node, which dispatches to the ManagementActor.
- **Central Host**: Serves the Management API at `POST /management`. Handles LDAP authentication, role resolution, and ManagementActor dispatch.

View File

@@ -2,7 +2,7 @@
## Purpose
The Management Service is an Akka.NET actor on the central cluster that provides programmatic access to all administrative operations. It exposes the same capabilities as the Central UI but through an actor-based interface, enabling the CLI (and potentially other tooling) to interact with the system without going through the web UI. The ManagementActor registers with ClusterClientReceptionist so that external processes can reach it via ClusterClient without joining the cluster.
The Management Service is an Akka.NET actor on the central cluster that provides programmatic access to all administrative operations. It exposes the same capabilities as the Central UI but through an actor-based interface, enabling the CLI (and potentially other tooling) to interact with the system without going through the web UI. The ManagementActor registers with ClusterClientReceptionist for cross-cluster access and is also exposed via an HTTP Management API endpoint (`POST /management`) for external tools like the CLI.
## Location
@@ -14,6 +14,7 @@ Central cluster only. The ManagementActor runs as a plain actor on **every** cen
- Provide an actor-based interface to all administrative operations available in the Central UI.
- Register with Akka.NET ClusterClientReceptionist so external tools (CLI) can discover and communicate with it via ClusterClient.
- Expose an HTTP API endpoint (`POST /management`) that accepts JSON commands with Basic Auth, performs LDAP authentication and role resolution, and dispatches to the ManagementActor.
- Validate and authorize all incoming commands using the authenticated user identity carried in message envelopes.
- Delegate to the appropriate services and repositories for each operation.
- Return structured response messages for all commands and queries.
@@ -25,6 +26,18 @@ Central cluster only. The ManagementActor runs as a plain actor on **every** cen
The central actor that receives and processes all management commands. Registered at a well-known actor path (`/user/management`) and with ClusterClientReceptionist.
### ManagementEndpoints
Minimal API endpoint (`POST /management`) that serves as the HTTP interface to the ManagementActor. Handles Basic Auth decoding, LDAP authentication via `LdapAuthService`, role resolution via `RoleMapper`, command deserialization via `ManagementCommandRegistry`, and ManagementActor dispatch.
### ManagementActorHolder
DI-registered singleton that holds the `IActorRef` for the ManagementActor. Set during actor registration in `AkkaHostedService` and injected into the HTTP endpoint handler.
### ManagementCommandRegistry
Static registry mapping command names (e.g., `"ListSites"`) to command types (e.g., `ListSitesCommand`). Built via reflection at startup. Used by the HTTP endpoint to deserialize JSON payloads into the correct command type.
### Message Contracts
All request/response messages are defined in **Commons** under `Messages/Management/`. Messages follow the existing additive-only evolution rules for version compatibility. Every request message includes:
@@ -36,6 +49,31 @@ All request/response messages are defined in **Commons** under `Messages/Managem
The ManagementActor registers itself with `ClusterClientReceptionist` at startup. This allows external processes using `ClusterClient` to send messages to the ManagementActor without joining the Akka.NET cluster as a full member. The receptionist advertises the actor under its well-known path.
## HTTP Management API
The Management Service also exposes a `POST /management` endpoint on the Central Host's web server. This provides an HTTP interface to the same ManagementActor, enabling the CLI (and other HTTP clients) to interact without Akka.NET dependencies.
**Request format:**
```json
POST /management
Authorization: Basic base64(username:password)
Content-Type: application/json
{
"command": "ListSites",
"payload": {}
}
```
**Response mapping:**
- `ManagementSuccess` → HTTP 200 with JSON body
- `ManagementError` → HTTP 400 with `{ "error": "...", "code": "..." }`
- `ManagementUnauthorized` → HTTP 403 with `{ "error": "...", "code": "UNAUTHORIZED" }`
- Authentication failure → HTTP 401
- Actor timeout → HTTP 504
The endpoint performs LDAP authentication and role resolution server-side, collapsing the CLI's previous two-step flow (ResolveRoles + actual command) into a single HTTP round-trip.
## Message Groups
### Templates
@@ -173,9 +211,9 @@ The ManagementActor receives the following services and repositories via DI (inj
## Interactions
- **CLI**: The primary consumer. Connects via Akka.NET ClusterClient and sends management messages to the ManagementActor.
- **CLI**: The primary consumer. Connects via the HTTP Management API (`POST /management`) and sends commands as JSON with Basic Auth credentials.
- **Host**: Registers the ManagementActor and ClusterClientReceptionist on central nodes during startup.
- **Central UI**: Shares the same underlying services and repositories. The ManagementActor and Central UI are parallel interfaces to the same operations.
- **Communication Layer**: Deployment commands and remote site queries flow through communication actors.
- **Configuration Database (via IAuditService)**: All configuration changes are audited.
- **Security & Auth**: The ManagementActor enforces authorization using user identity passed in messages. The CLI is responsible for authenticating the user and including their identity in every request.
- **Security & Auth**: The ManagementActor enforces authorization using user identity passed in messages. For HTTP API access, the Management endpoint authenticates the user via LDAP and resolves roles before dispatching to the ManagementActor.

View File

@@ -471,19 +471,19 @@ Sites log operational events locally, including:
### 13.1 Management Service
- The central cluster exposes a **ManagementActor** that provides programmatic access to all administrative operations — the same operations available through the Central UI.
- The ManagementActor registers with Akka.NET **ClusterClientReceptionist**, allowing external tools to communicate with it via ClusterClient without joining the cluster.
- The ManagementActor registers with Akka.NET **ClusterClientReceptionist** for cross-cluster access, and is also exposed via an HTTP Management API endpoint (`POST /management`) with Basic Auth, LDAP authentication, and role resolution — enabling external tools like the CLI to interact without Akka.NET dependencies.
- The ManagementActor enforces the **same role-based authorization** as the Central UI. Every incoming message carries the authenticated user's identity and roles.
- All mutating operations performed through the Management Service are **audit logged** via IAuditService, identical to operations performed through the Central UI.
- The ManagementActor runs on the **active central node** and fails over with it. ClusterClient handles reconnection transparently.
- The ManagementActor runs on **every central node** (stateless). For HTTP API access, any central node can handle any request without sticky sessions.
### 13.2 CLI
- The system provides a standalone **command-line tool** (`scadalink`) for scripting and automating administrative operations.
- The CLI connects to the ManagementActor via Akka.NET **ClusterClient** — it does not join the cluster as a full member and does not use HTTP/REST.
- The CLI authenticates the user against **LDAP/AD** (direct bind, same mechanism as the Central UI) and includes the authenticated identity in every message sent to the ManagementActor.
- The CLI connects to the Central Host's HTTP Management API (`POST /management`) — it sends commands as JSON with HTTP Basic Auth credentials. The server handles LDAP authentication, role resolution, and ManagementActor dispatch.
- The CLI sends user credentials via HTTP Basic Auth. The server authenticates against **LDAP/AD** and resolves roles before dispatching commands to the ManagementActor.
- CLI commands mirror all Management Service operations: templates, instances, sites, data connections, deployments, external systems, notifications, security (API keys and role mappings), audit log queries, and health status.
- Output is **JSON by default** (machine-readable, suitable for scripting) with an optional `--format table` flag for human-readable tabular output.
- Configuration is resolved from command-line options, **environment variables** (`SCADALINK_CONTACT_POINTS`, `SCADALINK_LDAP_SERVER`, etc.), or a **configuration file** (`~/.scadalink/config.json`).
- The CLI is a separate executable from the Host binary — it is deployed on any Windows machine with network access to the central cluster.
- Configuration is resolved from command-line options, **environment variables** (`SCADALINK_MANAGEMENT_URL`, `SCADALINK_FORMAT`), or a **configuration file** (`~/.scadalink/config.json`).
- The CLI is a separate executable from the Host binary — it is deployed on any machine with HTTP access to a central node.
## 14. General Conventions

View File

@@ -185,11 +185,11 @@ curl -s http://localhost:9002/health/ready | python3 -m json.tool
### CLI Access
Connect the ScadaLink CLI to the central cluster. With OrbStack, the contact point hostname must match the container's Akka `NodeHostname` config, so use the container name directly (OrbStack resolves container names via DNS):
The CLI connects to the Central Host's HTTP management API. With the Docker setup, the Central UI (and management API) is available at `http://localhost:9001`:
```bash
dotnet run --project src/ScadaLink.CLI -- \
--contact-points akka.tcp://scadalink@scadalink-central-a:8081 \
--url http://localhost:9001 \
--username multi-role --password password \
template list
```
@@ -200,19 +200,11 @@ A recommended `~/.scadalink/config.json` for the Docker test environment:
```json
{
"contactPoints": ["akka.tcp://scadalink@scadalink-central-a:8081"],
"ldap": {
"server": "localhost",
"port": 3893,
"useTls": false,
"searchBase": "dc=scadalink,dc=local",
"serviceAccountDn": "cn=admin,dc=scadalink,dc=local",
"serviceAccountPassword": "password"
}
"managementUrl": "http://localhost:9001"
}
```
With this config file in place, the contact points and LDAP settings are automatic:
With this config file in place, the URL is automatic:
```bash
dotnet run --project src/ScadaLink.CLI -- \

View File

@@ -4,13 +4,7 @@ namespace ScadaLink.CLI;
public class CliConfig
{
public List<string> ContactPoints { get; set; } = new();
public string? LdapServer { get; set; }
public int LdapPort { get; set; } = 636;
public bool LdapUseTls { get; set; } = true;
public string LdapSearchBase { get; set; } = string.Empty;
public string LdapServiceAccountDn { get; set; } = string.Empty;
public string LdapServiceAccountPassword { get; set; } = string.Empty;
public string? ManagementUrl { get; set; }
public string DefaultFormat { get; set; } = "json";
public static CliConfig Load()
@@ -28,51 +22,28 @@ public class CliConfig
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (fileConfig != null)
{
if (fileConfig.ContactPoints?.Count > 0) config.ContactPoints = fileConfig.ContactPoints;
if (fileConfig.Ldap != null)
{
config.LdapServer = fileConfig.Ldap.Server;
config.LdapPort = fileConfig.Ldap.Port;
config.LdapUseTls = fileConfig.Ldap.UseTls;
if (!string.IsNullOrEmpty(fileConfig.Ldap.SearchBase))
config.LdapSearchBase = fileConfig.Ldap.SearchBase;
if (!string.IsNullOrEmpty(fileConfig.Ldap.ServiceAccountDn))
config.LdapServiceAccountDn = fileConfig.Ldap.ServiceAccountDn;
if (!string.IsNullOrEmpty(fileConfig.Ldap.ServiceAccountPassword))
config.LdapServiceAccountPassword = fileConfig.Ldap.ServiceAccountPassword;
}
if (!string.IsNullOrEmpty(fileConfig.DefaultFormat)) config.DefaultFormat = fileConfig.DefaultFormat;
if (!string.IsNullOrEmpty(fileConfig.ManagementUrl))
config.ManagementUrl = fileConfig.ManagementUrl;
if (!string.IsNullOrEmpty(fileConfig.DefaultFormat))
config.DefaultFormat = fileConfig.DefaultFormat;
}
}
// Override from environment variables
var envContacts = Environment.GetEnvironmentVariable("SCADALINK_CONTACT_POINTS");
if (!string.IsNullOrEmpty(envContacts))
config.ContactPoints = envContacts.Split(',', StringSplitOptions.RemoveEmptyEntries).ToList();
var envLdap = Environment.GetEnvironmentVariable("SCADALINK_LDAP_SERVER");
if (!string.IsNullOrEmpty(envLdap)) config.LdapServer = envLdap;
var envUrl = Environment.GetEnvironmentVariable("SCADALINK_MANAGEMENT_URL");
if (!string.IsNullOrEmpty(envUrl))
config.ManagementUrl = envUrl;
var envFormat = Environment.GetEnvironmentVariable("SCADALINK_FORMAT");
if (!string.IsNullOrEmpty(envFormat)) config.DefaultFormat = envFormat;
if (!string.IsNullOrEmpty(envFormat))
config.DefaultFormat = envFormat;
return config;
}
private class CliConfigFile
{
public List<string>? ContactPoints { get; set; }
public LdapConfig? Ldap { get; set; }
public string? ManagementUrl { get; set; }
public string? DefaultFormat { get; set; }
}
private class LdapConfig
{
public string? Server { get; set; }
public int Port { get; set; } = 636;
public bool UseTls { get; set; } = true;
public string? SearchBase { get; set; }
public string? ServiceAccountDn { get; set; }
public string? ServiceAccountPassword { get; set; }
}
}

View File

@@ -1,63 +0,0 @@
using System.Collections.Immutable;
using Akka.Actor;
using Akka.Cluster.Tools.Client;
using Akka.Configuration;
using ScadaLink.Commons.Messages.Management;
namespace ScadaLink.CLI;
public class ClusterConnection : IAsyncDisposable
{
private ActorSystem? _system;
private IActorRef? _clusterClient;
public async Task ConnectAsync(IReadOnlyList<string> contactPoints, TimeSpan timeout)
{
var seedNodes = string.Join(",", contactPoints.Select(cp => $"\"{cp}\""));
var config = ConfigurationFactory.ParseString($@"
akka {{
actor.provider = remote
remote.dot-netty.tcp {{
hostname = ""127.0.0.1""
port = 0
}}
}}
");
_system = ActorSystem.Create("scadalink-cli", config);
var initialContacts = contactPoints
.Select(cp => $"{cp}/system/receptionist")
.Select(path => ActorPath.Parse(path))
.ToImmutableHashSet();
var clientSettings = ClusterClientSettings.Create(_system)
.WithInitialContacts(initialContacts);
_clusterClient = _system.ActorOf(ClusterClient.Props(clientSettings), "cluster-client");
// Wait for connection by sending a ping
// ClusterClient doesn't have a direct "connected" signal, so we rely on the first Ask succeeding
await Task.CompletedTask;
}
public async Task<object> AskManagementAsync(ManagementEnvelope envelope, TimeSpan timeout)
{
if (_clusterClient == null) throw new InvalidOperationException("Not connected");
var response = await _clusterClient.Ask(
new ClusterClient.Send("/user/management", envelope),
timeout);
return response;
}
public async ValueTask DisposeAsync()
{
if (_system != null)
{
await CoordinatedShutdown.Get(_system).Run(CoordinatedShutdown.ClrExitReason.Instance);
_system = null;
}
}
}

View File

@@ -6,31 +6,31 @@ namespace ScadaLink.CLI.Commands;
public static class ApiMethodCommands
{
public static Command Build(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var command = new Command("api-method") { Description = "Manage inbound API methods" };
command.Add(BuildList(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUpdate(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildList(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUpdate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(urlOption, formatOption, usernameOption, passwordOption));
return command;
}
private static Command BuildList(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildList(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var cmd = new Command("list") { Description = "List all API methods" };
cmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new ListApiMethodsCommand());
result, urlOption, formatOption, usernameOption, passwordOption, new ListApiMethodsCommand());
});
return cmd;
}
private static Command BuildGet(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildGet(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "API method ID", Required = true };
var cmd = new Command("get") { Description = "Get an API method by ID" };
@@ -39,12 +39,12 @@ public static class ApiMethodCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new GetApiMethodCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new GetApiMethodCommand(id));
});
return cmd;
}
private static Command BuildCreate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildCreate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var nameOption = new Option<string>("--name") { Description = "Method name", Required = true };
var scriptOption = new Option<string>("--script") { Description = "Script code", Required = true };
@@ -67,13 +67,13 @@ public static class ApiMethodCommands
var parameters = result.GetValue(parametersOption);
var returnDef = result.GetValue(returnDefOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new CreateApiMethodCommand(name, script, timeout, parameters, returnDef));
});
return cmd;
}
private static Command BuildUpdate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildUpdate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "API method ID", Required = true };
var scriptOption = new Option<string>("--script") { Description = "Script code", Required = true };
@@ -96,13 +96,13 @@ public static class ApiMethodCommands
var parameters = result.GetValue(parametersOption);
var returnDef = result.GetValue(returnDefOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new UpdateApiMethodCommand(id, script, timeout, parameters, returnDef));
});
return cmd;
}
private static Command BuildDelete(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildDelete(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "API method ID", Required = true };
var cmd = new Command("delete") { Description = "Delete an API method" };
@@ -111,7 +111,7 @@ public static class ApiMethodCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new DeleteApiMethodCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new DeleteApiMethodCommand(id));
});
return cmd;
}

View File

@@ -6,16 +6,16 @@ namespace ScadaLink.CLI.Commands;
public static class AuditLogCommands
{
public static Command Build(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var command = new Command("audit-log") { Description = "Query audit logs" };
command.Add(BuildQuery(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildQuery(urlOption, formatOption, usernameOption, passwordOption));
return command;
}
private static Command BuildQuery(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildQuery(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var userOption = new Option<string?>("--user") { Description = "Filter by username" };
var entityTypeOption = new Option<string?>("--entity-type") { Description = "Filter by entity type" };
@@ -45,7 +45,7 @@ public static class AuditLogCommands
var page = result.GetValue(pageOption);
var pageSize = result.GetValue(pageSizeOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new QueryAuditLogCommand(user, entityType, action, from, to, page, pageSize));
});
return cmd;

View File

@@ -1,46 +1,37 @@
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using ScadaLink.Commons.Messages.Management;
using ScadaLink.Security;
namespace ScadaLink.CLI.Commands;
internal static class CommandHelpers
{
internal static string NewCorrelationId() => Guid.NewGuid().ToString("N");
internal static async Task<int> ExecuteCommandAsync(
ParseResult result,
Option<string> contactPointsOption,
Option<string> urlOption,
Option<string> formatOption,
Option<string> usernameOption,
Option<string> passwordOption,
object command)
{
var contactPointsRaw = result.GetValue(contactPointsOption);
var format = result.GetValue(formatOption) ?? "json";
var config = CliConfig.Load();
if (string.IsNullOrWhiteSpace(contactPointsRaw))
{
if (config.ContactPoints.Count > 0)
contactPointsRaw = string.Join(",", config.ContactPoints);
}
// Resolve management URL
var url = result.GetValue(urlOption);
if (string.IsNullOrWhiteSpace(url))
url = config.ManagementUrl;
if (string.IsNullOrWhiteSpace(contactPointsRaw))
if (string.IsNullOrWhiteSpace(url))
{
OutputFormatter.WriteError("No contact points specified. Use --contact-points or set SCADALINK_CONTACT_POINTS.", "NO_CONTACT_POINTS");
OutputFormatter.WriteError(
"No management URL specified. Use --url, set SCADALINK_MANAGEMENT_URL, or add 'managementUrl' to ~/.scadalink/config.json.",
"NO_URL");
return 1;
}
var contactPoints = contactPointsRaw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
// Authenticate via LDAP
// Validate credentials
var username = result.GetValue(usernameOption);
var password = result.GetValue(passwordOption);
@@ -52,99 +43,36 @@ internal static class CommandHelpers
return 1;
}
// Authenticate against LDAP
var securityOptions = new SecurityOptions
{
LdapServer = config.LdapServer ?? string.Empty,
LdapPort = config.LdapPort,
LdapUseTls = config.LdapUseTls,
AllowInsecureLdap = !config.LdapUseTls,
LdapSearchBase = config.LdapSearchBase,
LdapServiceAccountDn = config.LdapServiceAccountDn,
LdapServiceAccountPassword = config.LdapServiceAccountPassword
};
// Derive command name from type
var commandName = ManagementCommandRegistry.GetCommandName(command.GetType());
var ldapAuth = new LdapAuthService(
Options.Create(securityOptions),
NullLogger<LdapAuthService>.Instance);
var authResult = await ldapAuth.AuthenticateAsync(username, password);
if (!authResult.Success)
{
OutputFormatter.WriteError(
authResult.ErrorMessage ?? "Authentication failed.",
"AUTH_FAILED");
return 1;
}
await using var connection = new ClusterConnection();
await connection.ConnectAsync(contactPoints, TimeSpan.FromSeconds(10));
// Resolve roles server-side
var resolveEnvelope = new ManagementEnvelope(
new AuthenticatedUser(authResult.Username!, authResult.DisplayName!, Array.Empty<string>(), Array.Empty<string>()),
new ResolveRolesCommand(authResult.Groups ?? (IReadOnlyList<string>)Array.Empty<string>()),
NewCorrelationId());
var resolveResponse = await connection.AskManagementAsync(resolveEnvelope, TimeSpan.FromSeconds(30));
string[] roles;
string[] permittedSiteIds;
if (resolveResponse is ManagementSuccess resolveSuccess)
{
var rolesDoc = JsonDocument.Parse(resolveSuccess.JsonData);
roles = rolesDoc.RootElement.TryGetProperty("Roles", out var rolesEl)
? rolesEl.EnumerateArray().Select(e => e.GetString()!).ToArray()
: Array.Empty<string>();
permittedSiteIds = rolesDoc.RootElement.TryGetProperty("PermittedSiteIds", out var sitesEl)
? sitesEl.EnumerateArray().Select(e => e.GetString()!).ToArray()
: Array.Empty<string>();
}
else
{
return HandleResponse(resolveResponse, format);
}
var authenticatedUser = new AuthenticatedUser(
authResult.Username!,
authResult.DisplayName!,
roles,
permittedSiteIds);
var envelope = new ManagementEnvelope(authenticatedUser, command, NewCorrelationId());
var response = await connection.AskManagementAsync(envelope, TimeSpan.FromSeconds(30));
// Send via HTTP
using var client = new ManagementHttpClient(url, username, password);
var response = await client.SendCommandAsync(commandName, command, TimeSpan.FromSeconds(30));
return HandleResponse(response, format);
}
internal static int HandleResponse(object response, string format)
internal static int HandleResponse(ManagementResponse response, string format)
{
switch (response)
if (response.JsonData != null)
{
case ManagementSuccess success:
if (string.Equals(format, "table", StringComparison.OrdinalIgnoreCase))
{
WriteAsTable(success.JsonData);
}
else
{
Console.WriteLine(success.JsonData);
}
return 0;
case ManagementError error:
OutputFormatter.WriteError(error.Error, error.ErrorCode);
return 1;
case ManagementUnauthorized unauth:
OutputFormatter.WriteError(unauth.Message, "UNAUTHORIZED");
return 2;
default:
OutputFormatter.WriteError($"Unexpected response type: {response.GetType().Name}", "UNEXPECTED_RESPONSE");
return 1;
if (string.Equals(format, "table", StringComparison.OrdinalIgnoreCase))
{
WriteAsTable(response.JsonData);
}
else
{
Console.WriteLine(response.JsonData);
}
return 0;
}
var errorCode = response.ErrorCode ?? "ERROR";
var error = response.Error ?? "Unknown error";
OutputFormatter.WriteError(error, errorCode);
return response.StatusCode == 403 ? 2 : 1;
}
private static void WriteAsTable(string json)
@@ -161,7 +89,6 @@ internal static class CommandHelpers
return;
}
// Extract headers from first object's property names
var headers = items[0].ValueKind == JsonValueKind.Object
? items[0].EnumerateObject().Select(p => p.Name).ToArray()
: new[] { "Value" };
@@ -182,7 +109,6 @@ internal static class CommandHelpers
}
else if (root.ValueKind == JsonValueKind.Object)
{
// Single object: render as key-value pairs
var headers = new[] { "Property", "Value" };
var rows = root.EnumerateObject().Select(p =>
new[] { p.Name, p.Value.ValueKind == JsonValueKind.Null ? "" : p.Value.ToString() });

View File

@@ -6,22 +6,22 @@ namespace ScadaLink.CLI.Commands;
public static class DataConnectionCommands
{
public static Command Build(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var command = new Command("data-connection") { Description = "Manage data connections" };
command.Add(BuildList(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUpdate(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildAssign(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUnassign(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildList(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUpdate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildAssign(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUnassign(urlOption, formatOption, usernameOption, passwordOption));
return command;
}
private static Command BuildGet(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildGet(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Data connection ID", Required = true };
var cmd = new Command("get") { Description = "Get a data connection by ID" };
@@ -30,12 +30,12 @@ public static class DataConnectionCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new GetDataConnectionCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new GetDataConnectionCommand(id));
});
return cmd;
}
private static Command BuildUpdate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildUpdate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Data connection ID", Required = true };
var nameOption = new Option<string>("--name") { Description = "Connection name", Required = true };
@@ -54,13 +54,13 @@ public static class DataConnectionCommands
var protocol = result.GetValue(protocolOption)!;
var config = result.GetValue(configOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new UpdateDataConnectionCommand(id, name, protocol, config));
});
return cmd;
}
private static Command BuildUnassign(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildUnassign(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--assignment-id") { Description = "Assignment ID", Required = true };
var cmd = new Command("unassign") { Description = "Unassign a data connection from a site" };
@@ -69,23 +69,23 @@ public static class DataConnectionCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new UnassignDataConnectionFromSiteCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new UnassignDataConnectionFromSiteCommand(id));
});
return cmd;
}
private static Command BuildList(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildList(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var cmd = new Command("list") { Description = "List all data connections" };
cmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new ListDataConnectionsCommand());
result, urlOption, formatOption, usernameOption, passwordOption, new ListDataConnectionsCommand());
});
return cmd;
}
private static Command BuildCreate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildCreate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var nameOption = new Option<string>("--name") { Description = "Connection name", Required = true };
var protocolOption = new Option<string>("--protocol") { Description = "Protocol (e.g. OpcUa)", Required = true };
@@ -101,13 +101,13 @@ public static class DataConnectionCommands
var protocol = result.GetValue(protocolOption)!;
var config = result.GetValue(configOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new CreateDataConnectionCommand(name, protocol, config));
});
return cmd;
}
private static Command BuildDelete(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildDelete(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Data connection ID", Required = true };
var cmd = new Command("delete") { Description = "Delete a data connection" };
@@ -116,12 +116,12 @@ public static class DataConnectionCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new DeleteDataConnectionCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new DeleteDataConnectionCommand(id));
});
return cmd;
}
private static Command BuildAssign(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildAssign(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var connectionIdOption = new Option<int>("--connection-id") { Description = "Data connection ID", Required = true };
var siteIdOption = new Option<int>("--site-id") { Description = "Site ID", Required = true };
@@ -134,7 +134,7 @@ public static class DataConnectionCommands
var connectionId = result.GetValue(connectionIdOption);
var siteId = result.GetValue(siteIdOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new AssignDataConnectionToSiteCommand(connectionId, siteId));
});
return cmd;

View File

@@ -6,31 +6,31 @@ namespace ScadaLink.CLI.Commands;
public static class DbConnectionCommands
{
public static Command Build(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var command = new Command("db-connection") { Description = "Manage database connections" };
command.Add(BuildList(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUpdate(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildList(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUpdate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(urlOption, formatOption, usernameOption, passwordOption));
return command;
}
private static Command BuildList(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildList(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var cmd = new Command("list") { Description = "List all database connections" };
cmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new ListDatabaseConnectionsCommand());
result, urlOption, formatOption, usernameOption, passwordOption, new ListDatabaseConnectionsCommand());
});
return cmd;
}
private static Command BuildGet(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildGet(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Database connection ID", Required = true };
var cmd = new Command("get") { Description = "Get a database connection by ID" };
@@ -39,12 +39,12 @@ public static class DbConnectionCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new GetDatabaseConnectionCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new GetDatabaseConnectionCommand(id));
});
return cmd;
}
private static Command BuildCreate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildCreate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var nameOption = new Option<string>("--name") { Description = "Connection name", Required = true };
var connStrOption = new Option<string>("--connection-string") { Description = "Connection string", Required = true };
@@ -57,13 +57,13 @@ public static class DbConnectionCommands
var name = result.GetValue(nameOption)!;
var connStr = result.GetValue(connStrOption)!;
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new CreateDatabaseConnectionDefCommand(name, connStr));
});
return cmd;
}
private static Command BuildUpdate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildUpdate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Database connection ID", Required = true };
var nameOption = new Option<string>("--name") { Description = "Connection name", Required = true };
@@ -79,13 +79,13 @@ public static class DbConnectionCommands
var name = result.GetValue(nameOption)!;
var connStr = result.GetValue(connStrOption)!;
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new UpdateDatabaseConnectionDefCommand(id, name, connStr));
});
return cmd;
}
private static Command BuildDelete(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildDelete(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Database connection ID", Required = true };
var cmd = new Command("delete") { Description = "Delete a database connection" };
@@ -94,7 +94,7 @@ public static class DbConnectionCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new DeleteDatabaseConnectionDefCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new DeleteDatabaseConnectionDefCommand(id));
});
return cmd;
}

View File

@@ -6,16 +6,16 @@ namespace ScadaLink.CLI.Commands;
public static class DebugCommands
{
public static Command Build(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var command = new Command("debug") { Description = "Runtime debugging" };
command.Add(BuildSnapshot(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildSnapshot(urlOption, formatOption, usernameOption, passwordOption));
return command;
}
private static Command BuildSnapshot(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildSnapshot(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Instance ID", Required = true };
var cmd = new Command("snapshot") { Description = "Get a point-in-time snapshot of instance attribute values and alarm states" };
@@ -23,7 +23,7 @@ public static class DebugCommands
cmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new DebugSnapshotCommand(result.GetValue(idOption)));
});
return cmd;

View File

@@ -6,18 +6,18 @@ namespace ScadaLink.CLI.Commands;
public static class DeployCommands
{
public static Command Build(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var command = new Command("deploy") { Description = "Deployment operations" };
command.Add(BuildInstance(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildArtifacts(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildStatus(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildInstance(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildArtifacts(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildStatus(urlOption, formatOption, usernameOption, passwordOption));
return command;
}
private static Command BuildInstance(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildInstance(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Instance ID", Required = true };
var cmd = new Command("instance") { Description = "Deploy a single instance" };
@@ -26,12 +26,12 @@ public static class DeployCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new MgmtDeployInstanceCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new MgmtDeployInstanceCommand(id));
});
return cmd;
}
private static Command BuildArtifacts(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildArtifacts(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var siteIdOption = new Option<int?>("--site-id") { Description = "Target site ID (all sites if omitted)" };
var cmd = new Command("artifacts") { Description = "Deploy artifacts to site(s)" };
@@ -40,12 +40,12 @@ public static class DeployCommands
{
var siteId = result.GetValue(siteIdOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new MgmtDeployArtifactsCommand(siteId));
result, urlOption, formatOption, usernameOption, passwordOption, new MgmtDeployArtifactsCommand(siteId));
});
return cmd;
}
private static Command BuildStatus(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildStatus(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var instanceIdOption = new Option<int?>("--instance-id") { Description = "Filter by instance ID" };
var statusOption = new Option<string?>("--status") { Description = "Filter by status" };
@@ -66,7 +66,7 @@ public static class DeployCommands
var page = result.GetValue(pageOption);
var pageSize = result.GetValue(pageSizeOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new QueryDeploymentsCommand(instanceId, status, page, pageSize));
});
return cmd;

View File

@@ -6,21 +6,21 @@ namespace ScadaLink.CLI.Commands;
public static class ExternalSystemCommands
{
public static Command Build(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var command = new Command("external-system") { Description = "Manage external systems" };
command.Add(BuildList(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUpdate(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildMethodGroup(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildList(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUpdate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildMethodGroup(urlOption, formatOption, usernameOption, passwordOption));
return command;
}
private static Command BuildGet(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildGet(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "External system ID", Required = true };
var cmd = new Command("get") { Description = "Get an external system by ID" };
@@ -29,76 +29,76 @@ public static class ExternalSystemCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new GetExternalSystemCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new GetExternalSystemCommand(id));
});
return cmd;
}
private static Command BuildUpdate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildUpdate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "External system ID", Required = true };
var nameOption = new Option<string>("--name") { Description = "System name", Required = true };
var urlOption = new Option<string>("--endpoint-url") { Description = "Endpoint URL", Required = true };
var endpointUrlOption = new Option<string>("--endpoint-url") { Description = "Endpoint URL", Required = true };
var authTypeOption = new Option<string>("--auth-type") { Description = "Auth type", Required = true };
var authConfigOption = new Option<string?>("--auth-config") { Description = "Auth configuration JSON" };
var cmd = new Command("update") { Description = "Update an external system" };
cmd.Add(idOption);
cmd.Add(nameOption);
cmd.Add(urlOption);
cmd.Add(endpointUrlOption);
cmd.Add(authTypeOption);
cmd.Add(authConfigOption);
cmd.SetAction(async (ParseResult result) =>
{
var id = result.GetValue(idOption);
var name = result.GetValue(nameOption)!;
var url = result.GetValue(urlOption)!;
var endpointUrl = result.GetValue(endpointUrlOption)!;
var authType = result.GetValue(authTypeOption)!;
var authConfig = result.GetValue(authConfigOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
new UpdateExternalSystemCommand(id, name, url, authType, authConfig));
result, urlOption, formatOption, usernameOption, passwordOption,
new UpdateExternalSystemCommand(id, name, endpointUrl, authType, authConfig));
});
return cmd;
}
private static Command BuildList(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildList(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var cmd = new Command("list") { Description = "List all external systems" };
cmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new ListExternalSystemsCommand());
result, urlOption, formatOption, usernameOption, passwordOption, new ListExternalSystemsCommand());
});
return cmd;
}
private static Command BuildCreate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildCreate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var nameOption = new Option<string>("--name") { Description = "System name", Required = true };
var urlOption = new Option<string>("--endpoint-url") { Description = "Endpoint URL", Required = true };
var endpointUrlOption = new Option<string>("--endpoint-url") { Description = "Endpoint URL", Required = true };
var authTypeOption = new Option<string>("--auth-type") { Description = "Auth type (ApiKey, BasicAuth)", Required = true };
var authConfigOption = new Option<string?>("--auth-config") { Description = "Auth configuration JSON" };
var cmd = new Command("create") { Description = "Create an external system" };
cmd.Add(nameOption);
cmd.Add(urlOption);
cmd.Add(endpointUrlOption);
cmd.Add(authTypeOption);
cmd.Add(authConfigOption);
cmd.SetAction(async (ParseResult result) =>
{
var name = result.GetValue(nameOption)!;
var url = result.GetValue(urlOption)!;
var endpointUrl = result.GetValue(endpointUrlOption)!;
var authType = result.GetValue(authTypeOption)!;
var authConfig = result.GetValue(authConfigOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
new CreateExternalSystemCommand(name, url, authType, authConfig));
result, urlOption, formatOption, usernameOption, passwordOption,
new CreateExternalSystemCommand(name, endpointUrl, authType, authConfig));
});
return cmd;
}
private static Command BuildDelete(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildDelete(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "External system ID", Required = true };
var cmd = new Command("delete") { Description = "Delete an external system" };
@@ -107,25 +107,25 @@ public static class ExternalSystemCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new DeleteExternalSystemCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new DeleteExternalSystemCommand(id));
});
return cmd;
}
// -- Method subcommands --
private static Command BuildMethodGroup(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildMethodGroup(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var group = new Command("method") { Description = "Manage external system methods" };
group.Add(BuildMethodList(contactPointsOption, formatOption, usernameOption, passwordOption));
group.Add(BuildMethodGet(contactPointsOption, formatOption, usernameOption, passwordOption));
group.Add(BuildMethodCreate(contactPointsOption, formatOption, usernameOption, passwordOption));
group.Add(BuildMethodUpdate(contactPointsOption, formatOption, usernameOption, passwordOption));
group.Add(BuildMethodDelete(contactPointsOption, formatOption, usernameOption, passwordOption));
group.Add(BuildMethodList(urlOption, formatOption, usernameOption, passwordOption));
group.Add(BuildMethodGet(urlOption, formatOption, usernameOption, passwordOption));
group.Add(BuildMethodCreate(urlOption, formatOption, usernameOption, passwordOption));
group.Add(BuildMethodUpdate(urlOption, formatOption, usernameOption, passwordOption));
group.Add(BuildMethodDelete(urlOption, formatOption, usernameOption, passwordOption));
return group;
}
private static Command BuildMethodList(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildMethodList(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var sysIdOption = new Option<int>("--external-system-id") { Description = "External system ID", Required = true };
var cmd = new Command("list") { Description = "List methods for an external system" };
@@ -133,13 +133,13 @@ public static class ExternalSystemCommands
cmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new ListExternalSystemMethodsCommand(result.GetValue(sysIdOption)));
});
return cmd;
}
private static Command BuildMethodGet(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildMethodGet(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Method ID", Required = true };
var cmd = new Command("get") { Description = "Get an external system method by ID" };
@@ -147,13 +147,13 @@ public static class ExternalSystemCommands
cmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new GetExternalSystemMethodCommand(result.GetValue(idOption)));
});
return cmd;
}
private static Command BuildMethodCreate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildMethodCreate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var sysIdOption = new Option<int>("--external-system-id") { Description = "External system ID", Required = true };
var nameOption = new Option<string>("--name") { Description = "Method name", Required = true };
@@ -172,7 +172,7 @@ public static class ExternalSystemCommands
cmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new CreateExternalSystemMethodCommand(
result.GetValue(sysIdOption),
result.GetValue(nameOption)!,
@@ -184,7 +184,7 @@ public static class ExternalSystemCommands
return cmd;
}
private static Command BuildMethodUpdate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildMethodUpdate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Method ID", Required = true };
var nameOption = new Option<string?>("--name") { Description = "Method name" };
@@ -203,7 +203,7 @@ public static class ExternalSystemCommands
cmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new UpdateExternalSystemMethodCommand(
result.GetValue(idOption),
result.GetValue(nameOption),
@@ -215,7 +215,7 @@ public static class ExternalSystemCommands
return cmd;
}
private static Command BuildMethodDelete(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildMethodDelete(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Method ID", Required = true };
var cmd = new Command("delete") { Description = "Delete an external system method" };
@@ -223,7 +223,7 @@ public static class ExternalSystemCommands
cmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new DeleteExternalSystemMethodCommand(result.GetValue(idOption)));
});
return cmd;

View File

@@ -6,30 +6,30 @@ namespace ScadaLink.CLI.Commands;
public static class HealthCommands
{
public static Command Build(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var command = new Command("health") { Description = "Health monitoring" };
command.Add(BuildSummary(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildSite(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildEventLog(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildParkedMessages(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildSummary(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildSite(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildEventLog(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildParkedMessages(urlOption, formatOption, usernameOption, passwordOption));
return command;
}
private static Command BuildSummary(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildSummary(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var cmd = new Command("summary") { Description = "Get system health summary" };
cmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new GetHealthSummaryCommand());
result, urlOption, formatOption, usernameOption, passwordOption, new GetHealthSummaryCommand());
});
return cmd;
}
private static Command BuildSite(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildSite(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var identifierOption = new Option<string>("--identifier") { Description = "Site identifier", Required = true };
var cmd = new Command("site") { Description = "Get health for a specific site" };
@@ -38,12 +38,12 @@ public static class HealthCommands
{
var identifier = result.GetValue(identifierOption)!;
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new GetSiteHealthCommand(identifier));
result, urlOption, formatOption, usernameOption, passwordOption, new GetSiteHealthCommand(identifier));
});
return cmd;
}
private static Command BuildEventLog(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildEventLog(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var siteOption = new Option<string>("--site") { Description = "Site identifier", Required = true };
var eventTypeOption = new Option<string?>("--event-type") { Description = "Filter by event type" };
@@ -70,7 +70,7 @@ public static class HealthCommands
cmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new QueryEventLogsCommand(
result.GetValue(siteOption)!,
result.GetValue(eventTypeOption),
@@ -85,7 +85,7 @@ public static class HealthCommands
return cmd;
}
private static Command BuildParkedMessages(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildParkedMessages(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var siteOption = new Option<string>("--site") { Description = "Site identifier", Required = true };
var pageOption = new Option<int>("--page") { Description = "Page number" };
@@ -100,7 +100,7 @@ public static class HealthCommands
cmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new QueryParkedMessagesCommand(
result.GetValue(siteOption)!,
result.GetValue(pageOption),

View File

@@ -6,26 +6,26 @@ namespace ScadaLink.CLI.Commands;
public static class InstanceCommands
{
public static Command Build(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var command = new Command("instance") { Description = "Manage instances" };
command.Add(BuildList(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildSetBindings(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildSetOverrides(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildSetArea(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDiff(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDeploy(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildEnable(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDisable(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildList(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildSetBindings(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildSetOverrides(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildSetArea(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDiff(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDeploy(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildEnable(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDisable(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(urlOption, formatOption, usernameOption, passwordOption));
return command;
}
private static Command BuildGet(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildGet(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Instance ID", Required = true };
var cmd = new Command("get") { Description = "Get an instance by ID" };
@@ -34,12 +34,12 @@ public static class InstanceCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new GetInstanceCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new GetInstanceCommand(id));
});
return cmd;
}
private static Command BuildSetBindings(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildSetBindings(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Instance ID", Required = true };
var bindingsOption = new Option<string>("--bindings") { Description = "JSON array of [attributeName, dataConnectionId] pairs", Required = true };
@@ -56,13 +56,13 @@ public static class InstanceCommands
var bindings = pairs.Select(p =>
(p[0].ToString()!, int.Parse(p[1].ToString()!))).ToList();
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new SetConnectionBindingsCommand(id, bindings));
});
return cmd;
}
private static Command BuildList(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildList(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var siteIdOption = new Option<int?>("--site-id") { Description = "Filter by site ID" };
var templateIdOption = new Option<int?>("--template-id") { Description = "Filter by template ID" };
@@ -78,13 +78,13 @@ public static class InstanceCommands
var templateId = result.GetValue(templateIdOption);
var search = result.GetValue(searchOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new ListInstancesCommand(siteId, templateId, search));
});
return cmd;
}
private static Command BuildCreate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildCreate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var nameOption = new Option<string>("--name") { Description = "Unique instance name", Required = true };
var templateIdOption = new Option<int>("--template-id") { Description = "Template ID", Required = true };
@@ -103,13 +103,13 @@ public static class InstanceCommands
var siteId = result.GetValue(siteIdOption);
var areaId = result.GetValue(areaIdOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new CreateInstanceCommand(name, templateId, siteId, areaId));
});
return cmd;
}
private static Command BuildDeploy(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildDeploy(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Instance ID", Required = true };
var cmd = new Command("deploy") { Description = "Deploy an instance" };
@@ -118,12 +118,12 @@ public static class InstanceCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new MgmtDeployInstanceCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new MgmtDeployInstanceCommand(id));
});
return cmd;
}
private static Command BuildEnable(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildEnable(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Instance ID", Required = true };
var cmd = new Command("enable") { Description = "Enable an instance" };
@@ -132,12 +132,12 @@ public static class InstanceCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new MgmtEnableInstanceCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new MgmtEnableInstanceCommand(id));
});
return cmd;
}
private static Command BuildDisable(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildDisable(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Instance ID", Required = true };
var cmd = new Command("disable") { Description = "Disable an instance" };
@@ -146,12 +146,12 @@ public static class InstanceCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new MgmtDisableInstanceCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new MgmtDisableInstanceCommand(id));
});
return cmd;
}
private static Command BuildDelete(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildDelete(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Instance ID", Required = true };
var cmd = new Command("delete") { Description = "Delete an instance" };
@@ -160,12 +160,12 @@ public static class InstanceCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new MgmtDeleteInstanceCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new MgmtDeleteInstanceCommand(id));
});
return cmd;
}
private static Command BuildSetOverrides(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildSetOverrides(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Instance ID", Required = true };
var overridesOption = new Option<string>("--overrides") { Description = "JSON object of attribute name -> value pairs, e.g. {\"Speed\": \"100\", \"Mode\": null}", Required = true };
@@ -180,13 +180,13 @@ public static class InstanceCommands
var overrides = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string?>>(overridesJson)
?? throw new InvalidOperationException("Invalid overrides JSON");
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new SetInstanceOverridesCommand(id, overrides));
});
return cmd;
}
private static Command BuildSetArea(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildSetArea(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Instance ID", Required = true };
var areaIdOption = new Option<int?>("--area-id") { Description = "Area ID (omit to clear area assignment)" };
@@ -199,13 +199,13 @@ public static class InstanceCommands
var id = result.GetValue(idOption);
var areaId = result.GetValue(areaIdOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new SetInstanceAreaCommand(id, areaId));
});
return cmd;
}
private static Command BuildDiff(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildDiff(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Instance ID", Required = true };
@@ -215,7 +215,7 @@ public static class InstanceCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new GetDeploymentDiffCommand(id));
});
return cmd;

View File

@@ -6,21 +6,21 @@ namespace ScadaLink.CLI.Commands;
public static class NotificationCommands
{
public static Command Build(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var command = new Command("notification") { Description = "Manage notification lists" };
command.Add(BuildList(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUpdate(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildSmtp(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildList(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUpdate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildSmtp(urlOption, formatOption, usernameOption, passwordOption));
return command;
}
private static Command BuildGet(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildGet(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Notification list ID", Required = true };
var cmd = new Command("get") { Description = "Get a notification list by ID" };
@@ -29,12 +29,12 @@ public static class NotificationCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new GetNotificationListCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new GetNotificationListCommand(id));
});
return cmd;
}
private static Command BuildUpdate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildUpdate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Notification list ID", Required = true };
var nameOption = new Option<string>("--name") { Description = "List name", Required = true };
@@ -51,13 +51,13 @@ public static class NotificationCommands
var emailsRaw = result.GetValue(emailsOption)!;
var emails = emailsRaw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new UpdateNotificationListCommand(id, name, emails));
});
return cmd;
}
private static Command BuildSmtp(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildSmtp(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var group = new Command("smtp") { Description = "Manage SMTP configuration" };
@@ -65,7 +65,7 @@ public static class NotificationCommands
listCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new ListSmtpConfigsCommand());
result, urlOption, formatOption, usernameOption, passwordOption, new ListSmtpConfigsCommand());
});
group.Add(listCmd);
@@ -88,7 +88,7 @@ public static class NotificationCommands
var authMode = result.GetValue(authModeOption)!;
var from = result.GetValue(fromOption)!;
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new UpdateSmtpConfigCommand(id, server, port, authMode, from));
});
group.Add(updateCmd);
@@ -96,18 +96,18 @@ public static class NotificationCommands
return group;
}
private static Command BuildList(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildList(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var cmd = new Command("list") { Description = "List all notification lists" };
cmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new ListNotificationListsCommand());
result, urlOption, formatOption, usernameOption, passwordOption, new ListNotificationListsCommand());
});
return cmd;
}
private static Command BuildCreate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildCreate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var nameOption = new Option<string>("--name") { Description = "Notification list name", Required = true };
var emailsOption = new Option<string>("--emails") { Description = "Comma-separated recipient emails", Required = true };
@@ -121,13 +121,13 @@ public static class NotificationCommands
var emailsRaw = result.GetValue(emailsOption)!;
var emails = emailsRaw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new CreateNotificationListCommand(name, emails));
});
return cmd;
}
private static Command BuildDelete(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildDelete(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Notification list ID", Required = true };
var cmd = new Command("delete") { Description = "Delete a notification list" };
@@ -136,7 +136,7 @@ public static class NotificationCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new DeleteNotificationListCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new DeleteNotificationListCommand(id));
});
return cmd;
}

View File

@@ -6,18 +6,18 @@ namespace ScadaLink.CLI.Commands;
public static class SecurityCommands
{
public static Command Build(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var command = new Command("security") { Description = "Manage security settings" };
command.Add(BuildApiKey(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildRoleMapping(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildScopeRule(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildApiKey(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildRoleMapping(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildScopeRule(urlOption, formatOption, usernameOption, passwordOption));
return command;
}
private static Command BuildApiKey(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildApiKey(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var group = new Command("api-key") { Description = "Manage API keys" };
@@ -25,7 +25,7 @@ public static class SecurityCommands
listCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new ListApiKeysCommand());
result, urlOption, formatOption, usernameOption, passwordOption, new ListApiKeysCommand());
});
group.Add(listCmd);
@@ -36,7 +36,7 @@ public static class SecurityCommands
{
var name = result.GetValue(nameOption)!;
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new CreateApiKeyCommand(name));
result, urlOption, formatOption, usernameOption, passwordOption, new CreateApiKeyCommand(name));
});
group.Add(createCmd);
@@ -47,7 +47,7 @@ public static class SecurityCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new DeleteApiKeyCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new DeleteApiKeyCommand(id));
});
group.Add(deleteCmd);
@@ -61,14 +61,14 @@ public static class SecurityCommands
var id = result.GetValue(updateIdOption);
var enabled = result.GetValue(enabledOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new UpdateApiKeyCommand(id, enabled));
result, urlOption, formatOption, usernameOption, passwordOption, new UpdateApiKeyCommand(id, enabled));
});
group.Add(updateCmd);
return group;
}
private static Command BuildRoleMapping(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildRoleMapping(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var group = new Command("role-mapping") { Description = "Manage LDAP role mappings" };
@@ -76,7 +76,7 @@ public static class SecurityCommands
listCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new ListRoleMappingsCommand());
result, urlOption, formatOption, usernameOption, passwordOption, new ListRoleMappingsCommand());
});
group.Add(listCmd);
@@ -90,7 +90,7 @@ public static class SecurityCommands
var ldapGroup = result.GetValue(ldapGroupOption)!;
var role = result.GetValue(roleOption)!;
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new CreateRoleMappingCommand(ldapGroup, role));
});
group.Add(createCmd);
@@ -102,7 +102,7 @@ public static class SecurityCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new DeleteRoleMappingCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new DeleteRoleMappingCommand(id));
});
group.Add(deleteCmd);
@@ -119,7 +119,7 @@ public static class SecurityCommands
var ldapGroup = result.GetValue(updateLdapGroupOption)!;
var role = result.GetValue(updateRoleOption)!;
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new UpdateRoleMappingCommand(id, ldapGroup, role));
});
group.Add(updateCmd);
@@ -127,7 +127,7 @@ public static class SecurityCommands
return group;
}
private static Command BuildScopeRule(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildScopeRule(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var group = new Command("scope-rule") { Description = "Manage LDAP scope rules" };
@@ -138,7 +138,7 @@ public static class SecurityCommands
{
var mappingId = result.GetValue(mappingIdOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new ListScopeRulesCommand(mappingId));
result, urlOption, formatOption, usernameOption, passwordOption, new ListScopeRulesCommand(mappingId));
});
group.Add(listCmd);
@@ -152,7 +152,7 @@ public static class SecurityCommands
var mappingId = result.GetValue(addMappingIdOption);
var siteId = result.GetValue(siteIdOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new AddScopeRuleCommand(mappingId, siteId));
result, urlOption, formatOption, usernameOption, passwordOption, new AddScopeRuleCommand(mappingId, siteId));
});
group.Add(addCmd);
@@ -163,7 +163,7 @@ public static class SecurityCommands
{
var id = result.GetValue(deleteIdOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new DeleteScopeRuleCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new DeleteScopeRuleCommand(id));
});
group.Add(deleteCmd);

View File

@@ -6,31 +6,31 @@ namespace ScadaLink.CLI.Commands;
public static class SharedScriptCommands
{
public static Command Build(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var command = new Command("shared-script") { Description = "Manage shared scripts" };
command.Add(BuildList(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUpdate(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildList(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUpdate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(urlOption, formatOption, usernameOption, passwordOption));
return command;
}
private static Command BuildList(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildList(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var cmd = new Command("list") { Description = "List all shared scripts" };
cmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new ListSharedScriptsCommand());
result, urlOption, formatOption, usernameOption, passwordOption, new ListSharedScriptsCommand());
});
return cmd;
}
private static Command BuildGet(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildGet(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Shared script ID", Required = true };
var cmd = new Command("get") { Description = "Get a shared script by ID" };
@@ -39,12 +39,12 @@ public static class SharedScriptCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new GetSharedScriptCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new GetSharedScriptCommand(id));
});
return cmd;
}
private static Command BuildCreate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildCreate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var nameOption = new Option<string>("--name") { Description = "Script name", Required = true };
var codeOption = new Option<string>("--code") { Description = "Script code", Required = true };
@@ -63,13 +63,13 @@ public static class SharedScriptCommands
var parameters = result.GetValue(parametersOption);
var returnDef = result.GetValue(returnDefOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new CreateSharedScriptCommand(name, code, parameters, returnDef));
});
return cmd;
}
private static Command BuildUpdate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildUpdate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Shared script ID", Required = true };
var nameOption = new Option<string>("--name") { Description = "Script name", Required = true };
@@ -91,13 +91,13 @@ public static class SharedScriptCommands
var parameters = result.GetValue(parametersOption);
var returnDef = result.GetValue(returnDefOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new UpdateSharedScriptCommand(id, name, code, parameters, returnDef));
});
return cmd;
}
private static Command BuildDelete(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildDelete(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Shared script ID", Required = true };
var cmd = new Command("delete") { Description = "Delete a shared script" };
@@ -106,7 +106,7 @@ public static class SharedScriptCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new DeleteSharedScriptCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new DeleteSharedScriptCommand(id));
});
return cmd;
}

View File

@@ -6,22 +6,22 @@ namespace ScadaLink.CLI.Commands;
public static class SiteCommands
{
public static Command Build(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var command = new Command("site") { Description = "Manage sites" };
command.Add(BuildList(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUpdate(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDeployArtifacts(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildArea(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildList(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUpdate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDeployArtifacts(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildArea(urlOption, formatOption, usernameOption, passwordOption));
return command;
}
private static Command BuildGet(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildGet(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Site ID", Required = true };
var cmd = new Command("get") { Description = "Get a site by ID" };
@@ -30,23 +30,23 @@ public static class SiteCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new GetSiteCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new GetSiteCommand(id));
});
return cmd;
}
private static Command BuildList(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildList(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var cmd = new Command("list") { Description = "List all sites" };
cmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new ListSitesCommand());
result, urlOption, formatOption, usernameOption, passwordOption, new ListSitesCommand());
});
return cmd;
}
private static Command BuildCreate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildCreate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var nameOption = new Option<string>("--name") { Description = "Site name", Required = true };
var identifierOption = new Option<string>("--identifier") { Description = "Site identifier", Required = true };
@@ -68,13 +68,13 @@ public static class SiteCommands
var nodeA = result.GetValue(nodeAOption);
var nodeB = result.GetValue(nodeBOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new CreateSiteCommand(name, identifier, desc, nodeA, nodeB));
});
return cmd;
}
private static Command BuildUpdate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildUpdate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Site ID", Required = true };
var nameOption = new Option<string>("--name") { Description = "Site name", Required = true };
@@ -96,13 +96,13 @@ public static class SiteCommands
var nodeA = result.GetValue(nodeAOption);
var nodeB = result.GetValue(nodeBOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new UpdateSiteCommand(id, name, desc, nodeA, nodeB));
});
return cmd;
}
private static Command BuildDelete(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildDelete(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Site ID", Required = true };
var cmd = new Command("delete") { Description = "Delete a site" };
@@ -111,12 +111,12 @@ public static class SiteCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new DeleteSiteCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new DeleteSiteCommand(id));
});
return cmd;
}
private static Command BuildArea(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildArea(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var group = new Command("area") { Description = "Manage areas" };
@@ -127,7 +127,7 @@ public static class SiteCommands
{
var siteId = result.GetValue(siteIdOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new ListAreasCommand(siteId));
result, urlOption, formatOption, usernameOption, passwordOption, new ListAreasCommand(siteId));
});
group.Add(listCmd);
@@ -144,7 +144,7 @@ public static class SiteCommands
var name = result.GetValue(nameOption)!;
var parentId = result.GetValue(parentOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new CreateAreaCommand(siteId, name, parentId));
});
group.Add(createCmd);
@@ -159,7 +159,7 @@ public static class SiteCommands
var id = result.GetValue(updateIdOption);
var name = result.GetValue(updateNameOption)!;
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new UpdateAreaCommand(id, name));
result, urlOption, formatOption, usernameOption, passwordOption, new UpdateAreaCommand(id, name));
});
group.Add(updateCmd);
@@ -170,14 +170,14 @@ public static class SiteCommands
{
var id = result.GetValue(deleteIdOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new DeleteAreaCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new DeleteAreaCommand(id));
});
group.Add(deleteCmd);
return group;
}
private static Command BuildDeployArtifacts(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildDeployArtifacts(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var siteIdOption = new Option<int?>("--site-id") { Description = "Target site ID (all sites if omitted)" };
var cmd = new Command("deploy-artifacts") { Description = "Deploy artifacts to site(s)" };
@@ -186,7 +186,7 @@ public static class SiteCommands
{
var siteId = result.GetValue(siteIdOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new MgmtDeployArtifactsCommand(siteId));
result, urlOption, formatOption, usernameOption, passwordOption, new MgmtDeployArtifactsCommand(siteId));
});
return cmd;
}

View File

@@ -6,36 +6,36 @@ namespace ScadaLink.CLI.Commands;
public static class TemplateCommands
{
public static Command Build(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
public static Command Build(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var command = new Command("template") { Description = "Manage templates" };
command.Add(BuildList(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUpdate(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildValidate(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildAttribute(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildAlarm(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildScript(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildComposition(contactPointsOption, formatOption, usernameOption, passwordOption));
command.Add(BuildList(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildGet(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildCreate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildUpdate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildValidate(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildDelete(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildAttribute(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildAlarm(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildScript(urlOption, formatOption, usernameOption, passwordOption));
command.Add(BuildComposition(urlOption, formatOption, usernameOption, passwordOption));
return command;
}
private static Command BuildList(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildList(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var cmd = new Command("list") { Description = "List all templates" };
cmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new ListTemplatesCommand());
result, urlOption, formatOption, usernameOption, passwordOption, new ListTemplatesCommand());
});
return cmd;
}
private static Command BuildGet(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildGet(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Template ID", Required = true };
var cmd = new Command("get") { Description = "Get a template by ID" };
@@ -44,12 +44,12 @@ public static class TemplateCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new GetTemplateCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new GetTemplateCommand(id));
});
return cmd;
}
private static Command BuildCreate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildCreate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var nameOption = new Option<string>("--name") { Description = "Template name", Required = true };
var descOption = new Option<string?>("--description") { Description = "Template description" };
@@ -65,13 +65,13 @@ public static class TemplateCommands
var desc = result.GetValue(descOption);
var parentId = result.GetValue(parentOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new CreateTemplateCommand(name, desc, parentId));
});
return cmd;
}
private static Command BuildUpdate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildUpdate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Template ID", Required = true };
var nameOption = new Option<string>("--name") { Description = "Template name", Required = true };
@@ -90,13 +90,13 @@ public static class TemplateCommands
var desc = result.GetValue(descOption);
var parentId = result.GetValue(parentOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new UpdateTemplateCommand(id, name, desc, parentId));
});
return cmd;
}
private static Command BuildValidate(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildValidate(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Template ID", Required = true };
var cmd = new Command("validate") { Description = "Validate a template" };
@@ -105,12 +105,12 @@ public static class TemplateCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new ValidateTemplateCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new ValidateTemplateCommand(id));
});
return cmd;
}
private static Command BuildDelete(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildDelete(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var idOption = new Option<int>("--id") { Description = "Template ID", Required = true };
var cmd = new Command("delete") { Description = "Delete a template" };
@@ -119,12 +119,12 @@ public static class TemplateCommands
{
var id = result.GetValue(idOption);
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption, new DeleteTemplateCommand(id));
result, urlOption, formatOption, usernameOption, passwordOption, new DeleteTemplateCommand(id));
});
return cmd;
}
private static Command BuildAttribute(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildAttribute(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var group = new Command("attribute") { Description = "Manage template attributes" };
@@ -148,7 +148,7 @@ public static class TemplateCommands
addCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new AddTemplateAttributeCommand(
result.GetValue(templateIdOption),
result.GetValue(nameOption)!,
@@ -180,7 +180,7 @@ public static class TemplateCommands
updateCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new UpdateTemplateAttributeCommand(
result.GetValue(updateIdOption),
result.GetValue(updateNameOption)!,
@@ -198,7 +198,7 @@ public static class TemplateCommands
deleteCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new DeleteTemplateAttributeCommand(result.GetValue(deleteIdOption)));
});
group.Add(deleteCmd);
@@ -206,7 +206,7 @@ public static class TemplateCommands
return group;
}
private static Command BuildAlarm(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildAlarm(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var group = new Command("alarm") { Description = "Manage template alarms" };
@@ -230,7 +230,7 @@ public static class TemplateCommands
addCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new AddTemplateAlarmCommand(
result.GetValue(templateIdOption),
result.GetValue(nameOption)!,
@@ -262,7 +262,7 @@ public static class TemplateCommands
updateCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new UpdateTemplateAlarmCommand(
result.GetValue(updateIdOption),
result.GetValue(updateNameOption)!,
@@ -280,7 +280,7 @@ public static class TemplateCommands
deleteCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new DeleteTemplateAlarmCommand(result.GetValue(deleteIdOption)));
});
group.Add(deleteCmd);
@@ -288,7 +288,7 @@ public static class TemplateCommands
return group;
}
private static Command BuildScript(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildScript(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var group = new Command("script") { Description = "Manage template scripts" };
@@ -315,7 +315,7 @@ public static class TemplateCommands
addCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new AddTemplateScriptCommand(
result.GetValue(templateIdOption),
result.GetValue(nameOption)!,
@@ -351,7 +351,7 @@ public static class TemplateCommands
updateCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new UpdateTemplateScriptCommand(
result.GetValue(updateIdOption),
result.GetValue(updateNameOption)!,
@@ -370,7 +370,7 @@ public static class TemplateCommands
deleteCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new DeleteTemplateScriptCommand(result.GetValue(deleteIdOption)));
});
group.Add(deleteCmd);
@@ -378,7 +378,7 @@ public static class TemplateCommands
return group;
}
private static Command BuildComposition(Option<string> contactPointsOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
private static Command BuildComposition(Option<string> urlOption, Option<string> formatOption, Option<string> usernameOption, Option<string> passwordOption)
{
var group = new Command("composition") { Description = "Manage template compositions" };
@@ -393,7 +393,7 @@ public static class TemplateCommands
addCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new AddTemplateCompositionCommand(
result.GetValue(templateIdOption),
result.GetValue(instanceNameOption)!,
@@ -407,7 +407,7 @@ public static class TemplateCommands
deleteCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
result, contactPointsOption, formatOption, usernameOption, passwordOption,
result, urlOption, formatOption, usernameOption, passwordOption,
new DeleteTemplateCompositionCommand(result.GetValue(deleteIdOption)));
});
group.Add(deleteCmd);

View File

@@ -0,0 +1,69 @@
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
namespace ScadaLink.CLI;
public class ManagementHttpClient : IDisposable
{
private readonly HttpClient _httpClient;
public ManagementHttpClient(string baseUrl, string username, string password)
{
_httpClient = new HttpClient { BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/") };
var credentials = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"));
_httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", credentials);
}
public async Task<ManagementResponse> SendCommandAsync(string commandName, object payload, TimeSpan timeout)
{
using var cts = new CancellationTokenSource(timeout);
var body = JsonSerializer.Serialize(new { command = commandName, payload },
new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
var content = new StringContent(body, Encoding.UTF8, "application/json");
HttpResponseMessage httpResponse;
try
{
httpResponse = await _httpClient.PostAsync("management", content, cts.Token);
}
catch (TaskCanceledException)
{
return new ManagementResponse(504, null, "Request timed out.", "TIMEOUT");
}
catch (HttpRequestException ex)
{
return new ManagementResponse(0, null, $"Connection failed: {ex.Message}", "CONNECTION_FAILED");
}
var responseBody = await httpResponse.Content.ReadAsStringAsync(cts.Token);
if (httpResponse.IsSuccessStatusCode)
{
return new ManagementResponse((int)httpResponse.StatusCode, responseBody, null, null);
}
// Parse error response
string? error = null;
string? code = null;
try
{
using var doc = JsonDocument.Parse(responseBody);
error = doc.RootElement.TryGetProperty("error", out var e) ? e.GetString() : responseBody;
code = doc.RootElement.TryGetProperty("code", out var c) ? c.GetString() : null;
}
catch
{
error = responseBody;
}
return new ManagementResponse((int)httpResponse.StatusCode, null, error, code);
}
public void Dispose() => _httpClient.Dispose();
}
public record ManagementResponse(int StatusCode, string? JsonData, string? Error, string? ErrorCode);

View File

@@ -4,32 +4,32 @@ using ScadaLink.CLI.Commands;
var rootCommand = new RootCommand("ScadaLink CLI — manage the ScadaLink SCADA system");
var contactPointsOption = new Option<string>("--contact-points") { Description = "Comma-separated cluster contact points", Recursive = true };
var urlOption = new Option<string>("--url") { Description = "Management API URL", Recursive = true };
var usernameOption = new Option<string>("--username") { Description = "LDAP username", Recursive = true };
var passwordOption = new Option<string>("--password") { Description = "LDAP password", Recursive = true };
var formatOption = new Option<string>("--format") { Description = "Output format (json or table)", Recursive = true };
formatOption.DefaultValueFactory = _ => "json";
rootCommand.Add(contactPointsOption);
rootCommand.Add(urlOption);
rootCommand.Add(usernameOption);
rootCommand.Add(passwordOption);
rootCommand.Add(formatOption);
// Register command groups
rootCommand.Add(TemplateCommands.Build(contactPointsOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(InstanceCommands.Build(contactPointsOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(SiteCommands.Build(contactPointsOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(DeployCommands.Build(contactPointsOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(DataConnectionCommands.Build(contactPointsOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(ExternalSystemCommands.Build(contactPointsOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(NotificationCommands.Build(contactPointsOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(SecurityCommands.Build(contactPointsOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(AuditLogCommands.Build(contactPointsOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(HealthCommands.Build(contactPointsOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(DebugCommands.Build(contactPointsOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(SharedScriptCommands.Build(contactPointsOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(DbConnectionCommands.Build(contactPointsOption, formatOption, usernameOption, passwordOption));
rootCommand.Add(ApiMethodCommands.Build(contactPointsOption, formatOption, usernameOption, passwordOption));
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(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.SetAction(_ =>
{

View File

@@ -1,6 +1,6 @@
# ScadaLink CLI
Command-line tool for managing the ScadaLink SCADA system. Connects to a Central node via Akka.NET ClusterClient and routes commands through the ManagementActor.
Command-line tool for managing the ScadaLink SCADA system. Connects to a Central node via HTTP and routes commands through the Management API endpoint (`POST /management`), which dispatches to the ManagementActor. Authentication is handled server-side via LDAP.
## Installation
@@ -12,27 +12,23 @@ The output binary is `scadalink` (or `scadalink.exe` on Windows).
## Connection
Every command requires a connection to a running Central node. Contact points can be supplied three ways, evaluated in this priority order:
Every command requires a connection to a running Central node. The management URL can be supplied three ways, evaluated in this priority order:
1. `--contact-points` flag on the command line
2. `SCADALINK_CONTACT_POINTS` environment variable
3. `contactPoints` array in `~/.scadalink/config.json`
1. `--url` flag on the command line
2. `SCADALINK_MANAGEMENT_URL` environment variable
3. `managementUrl` field in `~/.scadalink/config.json`
```sh
scadalink --contact-points akka.tcp://scadalink@central-host:8081 <command>
scadalink --url http://central-host:5000 <command>
```
For a two-node HA cluster, supply both nodes comma-separated:
**Docker:** With the Docker setup, the Central UI (and management API) is available at `http://localhost:9001`:
```sh
scadalink --contact-points akka.tcp://scadalink@node1:8081,akka.tcp://scadalink@node2:8082 <command>
scadalink --url http://localhost:9001 <command>
```
**Docker (OrbStack):** When running against Docker containers, the contact point hostname must match the central container's Akka `NodeHostname` config. With OrbStack, add `/etc/hosts` entries mapping the container names to their IPs (see `docker/README.md`). Example:
```sh
scadalink --contact-points akka.tcp://scadalink@scadalink-central-a:8081 <command>
```
For HA failover, use a load balancer URL. The management API is stateless (Basic Auth per request), so any central node can handle any request without sticky sessions.
## Global Options
@@ -40,7 +36,7 @@ These options are accepted by the root command and inherited by all subcommands.
| Option | Description |
|--------|-------------|
| `--contact-points <value>` | Comma-separated Akka cluster contact point URIs |
| `--url <value>` | Management API URL (e.g., `http://localhost:9001`) |
| `--username <value>` | LDAP username for authentication |
| `--password <value>` | LDAP password for authentication |
| `--format <json\|table>` | Output format (default: `json`) |
@@ -51,29 +47,17 @@ These options are accepted by the root command and inherited by all subcommands.
```json
{
"contactPoints": ["akka.tcp://scadalink@central-host:8081"],
"ldap": {
"server": "ldap.company.com",
"port": 636,
"useTls": true,
"searchBase": "dc=example,dc=com",
"serviceAccountDn": "cn=admin,dc=example,dc=com",
"serviceAccountPassword": "secret"
},
"defaultFormat": "json"
"managementUrl": "http://localhost:9001"
}
```
The `searchBase` and `serviceAccountDn`/`serviceAccountPassword` fields are required for LDAP servers that need search-then-bind authentication (including the test GLAuth server). Without them, direct bind with `cn={username},{searchBase}` is attempted, which may fail if the user's DN doesn't follow that pattern.
For the Docker test environment, see `docker/README.md` for a ready-to-use config.
## Environment Variables
| Variable | Description |
|----------|-------------|
| `SCADALINK_CONTACT_POINTS` | Comma-separated contact point URIs (overrides config file) |
| `SCADALINK_LDAP_SERVER` | LDAP server hostname (overrides config file) |
| `SCADALINK_MANAGEMENT_URL` | Management API URL (overrides config file) |
| `SCADALINK_FORMAT` | Default output format (overrides config file) |
## Output
@@ -103,7 +87,7 @@ Exit codes:
List all templates with their full attribute, alarm, script, and composition definitions.
```sh
scadalink --contact-points <uri> template list
scadalink --url <url> template list
```
#### `template get`
@@ -111,7 +95,7 @@ scadalink --contact-points <uri> template list
Get a single template by ID.
```sh
scadalink --contact-points <uri> template get --id <int>
scadalink --url <url> template get --id <int>
```
| Option | Required | Description |
@@ -123,7 +107,7 @@ scadalink --contact-points <uri> template get --id <int>
Create a new template, optionally inheriting from a parent.
```sh
scadalink --contact-points <uri> template create --name <string> [--description <string>] [--parent-id <int>]
scadalink --url <url> template create --name <string> [--description <string>] [--parent-id <int>]
```
| Option | Required | Description |
@@ -137,7 +121,7 @@ scadalink --contact-points <uri> template create --name <string> [--description
Update an existing template's name, description, or parent.
```sh
scadalink --contact-points <uri> template update --id <int> [--name <string>] [--description <string>] [--parent-id <int>]
scadalink --url <url> template update --id <int> [--name <string>] [--description <string>] [--parent-id <int>]
```
| Option | Required | Description |
@@ -152,7 +136,7 @@ scadalink --contact-points <uri> template update --id <int> [--name <string>] [-
Delete a template by ID.
```sh
scadalink --contact-points <uri> template delete --id <int>
scadalink --url <url> template delete --id <int>
```
| Option | Required | Description |
@@ -164,7 +148,7 @@ scadalink --contact-points <uri> template delete --id <int>
Run pre-deployment validation on a template (flattening, naming collisions, script compilation).
```sh
scadalink --contact-points <uri> template validate --id <int>
scadalink --url <url> template validate --id <int>
```
| Option | Required | Description |
@@ -176,7 +160,7 @@ scadalink --contact-points <uri> template validate --id <int>
Add an attribute to a template.
```sh
scadalink --contact-points <uri> template attribute add --template-id <int> --name <string> --data-type <string> [--default-value <string>] [--tag-path <string>]
scadalink --url <url> template attribute add --template-id <int> --name <string> --data-type <string> [--default-value <string>] [--tag-path <string>]
```
| Option | Required | Description |
@@ -192,7 +176,7 @@ scadalink --contact-points <uri> template attribute add --template-id <int> --na
Update an attribute on a template.
```sh
scadalink --contact-points <uri> template attribute update --template-id <int> --name <string> [--data-type <string>] [--default-value <string>] [--tag-path <string>]
scadalink --url <url> template attribute update --template-id <int> --name <string> [--data-type <string>] [--default-value <string>] [--tag-path <string>]
```
| Option | Required | Description |
@@ -208,7 +192,7 @@ scadalink --contact-points <uri> template attribute update --template-id <int> -
Remove an attribute from a template.
```sh
scadalink --contact-points <uri> template attribute delete --template-id <int> --name <string>
scadalink --url <url> template attribute delete --template-id <int> --name <string>
```
| Option | Required | Description |
@@ -221,7 +205,7 @@ scadalink --contact-points <uri> template attribute delete --template-id <int> -
Add an alarm definition to a template.
```sh
scadalink --contact-points <uri> template alarm add --template-id <int> --name <string> --trigger-attribute <string> --condition <string> --setpoint <string> [--severity <string>] [--notification-list <string>]
scadalink --url <url> template alarm add --template-id <int> --name <string> --trigger-attribute <string> --condition <string> --setpoint <string> [--severity <string>] [--notification-list <string>]
```
| Option | Required | Description |
@@ -239,7 +223,7 @@ scadalink --contact-points <uri> template alarm add --template-id <int> --name <
Update an alarm definition on a template.
```sh
scadalink --contact-points <uri> template alarm update --template-id <int> --name <string> [--condition <string>] [--setpoint <string>] [--severity <string>] [--notification-list <string>]
scadalink --url <url> template alarm update --template-id <int> --name <string> [--condition <string>] [--setpoint <string>] [--severity <string>] [--notification-list <string>]
```
| Option | Required | Description |
@@ -256,7 +240,7 @@ scadalink --contact-points <uri> template alarm update --template-id <int> --nam
Remove an alarm definition from a template.
```sh
scadalink --contact-points <uri> template alarm delete --template-id <int> --name <string>
scadalink --url <url> template alarm delete --template-id <int> --name <string>
```
| Option | Required | Description |
@@ -269,7 +253,7 @@ scadalink --contact-points <uri> template alarm delete --template-id <int> --nam
Add a script to a template.
```sh
scadalink --contact-points <uri> template script add --template-id <int> --name <string> --trigger-type <string> [--trigger-attribute <string>] [--interval <int>] --code <string>
scadalink --url <url> template script add --template-id <int> --name <string> --trigger-type <string> [--trigger-attribute <string>] [--interval <int>] --code <string>
```
| Option | Required | Description |
@@ -286,7 +270,7 @@ scadalink --contact-points <uri> template script add --template-id <int> --name
Update a script on a template.
```sh
scadalink --contact-points <uri> template script update --template-id <int> --name <string> [--trigger-type <string>] [--trigger-attribute <string>] [--interval <int>] [--code <string>]
scadalink --url <url> template script update --template-id <int> --name <string> [--trigger-type <string>] [--trigger-attribute <string>] [--interval <int>] [--code <string>]
```
| Option | Required | Description |
@@ -303,7 +287,7 @@ scadalink --contact-points <uri> template script update --template-id <int> --na
Remove a script from a template.
```sh
scadalink --contact-points <uri> template script delete --template-id <int> --name <string>
scadalink --url <url> template script delete --template-id <int> --name <string>
```
| Option | Required | Description |
@@ -316,7 +300,7 @@ scadalink --contact-points <uri> template script delete --template-id <int> --na
Add a feature module composition to a template.
```sh
scadalink --contact-points <uri> template composition add --template-id <int> --module-template-id <int> --instance-name <string>
scadalink --url <url> template composition add --template-id <int> --module-template-id <int> --instance-name <string>
```
| Option | Required | Description |
@@ -330,7 +314,7 @@ scadalink --contact-points <uri> template composition add --template-id <int> --
Remove a feature module composition from a template.
```sh
scadalink --contact-points <uri> template composition delete --template-id <int> --instance-name <string>
scadalink --url <url> template composition delete --template-id <int> --instance-name <string>
```
| Option | Required | Description |
@@ -347,7 +331,7 @@ scadalink --contact-points <uri> template composition delete --template-id <int>
Get a single instance by ID.
```sh
scadalink --contact-points <uri> instance get --id <int>
scadalink --url <url> instance get --id <int>
```
| Option | Required | Description |
@@ -359,7 +343,7 @@ scadalink --contact-points <uri> instance get --id <int>
List instances, with optional filters.
```sh
scadalink --contact-points <uri> instance list [--site-id <int>] [--template-id <int>] [--search <string>]
scadalink --url <url> instance list [--site-id <int>] [--template-id <int>] [--search <string>]
```
| Option | Required | Description |
@@ -373,7 +357,7 @@ scadalink --contact-points <uri> instance list [--site-id <int>] [--template-id
Create a new instance of a template at a site.
```sh
scadalink --contact-points <uri> instance create --name <string> --template-id <int> --site-id <int> [--area-id <int>]
scadalink --url <url> instance create --name <string> --template-id <int> --site-id <int> [--area-id <int>]
```
| Option | Required | Description |
@@ -388,7 +372,7 @@ scadalink --contact-points <uri> instance create --name <string> --template-id <
Deploy an instance to its site. Acquires the per-instance operation lock.
```sh
scadalink --contact-points <uri> instance deploy --id <int>
scadalink --url <url> instance deploy --id <int>
```
| Option | Required | Description |
@@ -400,7 +384,7 @@ scadalink --contact-points <uri> instance deploy --id <int>
Enable a previously disabled instance.
```sh
scadalink --contact-points <uri> instance enable --id <int>
scadalink --url <url> instance enable --id <int>
```
| Option | Required | Description |
@@ -412,7 +396,7 @@ scadalink --contact-points <uri> instance enable --id <int>
Disable a running instance without deleting it.
```sh
scadalink --contact-points <uri> instance disable --id <int>
scadalink --url <url> instance disable --id <int>
```
| Option | Required | Description |
@@ -424,7 +408,7 @@ scadalink --contact-points <uri> instance disable --id <int>
Delete an instance. The instance must be disabled first.
```sh
scadalink --contact-points <uri> instance delete --id <int>
scadalink --url <url> instance delete --id <int>
```
| Option | Required | Description |
@@ -436,7 +420,7 @@ scadalink --contact-points <uri> instance delete --id <int>
Set data connection bindings for an instance's attributes.
```sh
scadalink --contact-points <uri> instance set-bindings --id <int> --bindings <json>
scadalink --url <url> instance set-bindings --id <int> --bindings <json>
```
| Option | Required | Description |
@@ -453,7 +437,7 @@ scadalink --contact-points <uri> instance set-bindings --id <int> --bindings <js
Get a single site by ID.
```sh
scadalink --contact-points <uri> site get --id <int>
scadalink --url <url> site get --id <int>
```
| Option | Required | Description |
@@ -465,7 +449,7 @@ scadalink --contact-points <uri> site get --id <int>
List all registered sites.
```sh
scadalink --contact-points <uri> site list
scadalink --url <url> site list
```
#### `site create`
@@ -473,7 +457,7 @@ scadalink --contact-points <uri> site list
Register a new site.
```sh
scadalink --contact-points <uri> site create --name <string> --identifier <string> [--description <string>]
scadalink --url <url> site create --name <string> --identifier <string> [--description <string>]
```
| Option | Required | Description |
@@ -487,7 +471,7 @@ scadalink --contact-points <uri> site create --name <string> --identifier <strin
Delete a site. Fails if any instances are assigned to it.
```sh
scadalink --contact-points <uri> site delete --id <int>
scadalink --url <url> site delete --id <int>
```
| Option | Required | Description |
@@ -499,7 +483,7 @@ scadalink --contact-points <uri> site delete --id <int>
Push compiled artifacts to one or all sites.
```sh
scadalink --contact-points <uri> site deploy-artifacts [--site-id <int>]
scadalink --url <url> site deploy-artifacts [--site-id <int>]
```
| Option | Required | Description |
@@ -511,7 +495,7 @@ scadalink --contact-points <uri> site deploy-artifacts [--site-id <int>]
List all areas for a site.
```sh
scadalink --contact-points <uri> site area list --site-id <int>
scadalink --url <url> site area list --site-id <int>
```
| Option | Required | Description |
@@ -523,7 +507,7 @@ scadalink --contact-points <uri> site area list --site-id <int>
Create an area within a site.
```sh
scadalink --contact-points <uri> site area create --site-id <int> --name <string> [--parent-area-id <int>]
scadalink --url <url> site area create --site-id <int> --name <string> [--parent-area-id <int>]
```
| Option | Required | Description |
@@ -537,7 +521,7 @@ scadalink --contact-points <uri> site area create --site-id <int> --name <string
Update an area's name or parent.
```sh
scadalink --contact-points <uri> site area update --id <int> [--name <string>] [--parent-area-id <int>]
scadalink --url <url> site area update --id <int> [--name <string>] [--parent-area-id <int>]
```
| Option | Required | Description |
@@ -551,7 +535,7 @@ scadalink --contact-points <uri> site area update --id <int> [--name <string>] [
Delete an area. Fails if any instances are assigned to it.
```sh
scadalink --contact-points <uri> site area delete --id <int>
scadalink --url <url> site area delete --id <int>
```
| Option | Required | Description |
@@ -567,7 +551,7 @@ scadalink --contact-points <uri> site area delete --id <int>
Deploy a single instance (same as `instance deploy`).
```sh
scadalink --contact-points <uri> deploy instance --id <int>
scadalink --url <url> deploy instance --id <int>
```
| Option | Required | Description |
@@ -579,7 +563,7 @@ scadalink --contact-points <uri> deploy instance --id <int>
Deploy compiled artifacts to one or all sites (same as `site deploy-artifacts`).
```sh
scadalink --contact-points <uri> deploy artifacts [--site-id <int>]
scadalink --url <url> deploy artifacts [--site-id <int>]
```
| Option | Required | Description |
@@ -591,7 +575,7 @@ scadalink --contact-points <uri> deploy artifacts [--site-id <int>]
Query deployment records, with optional filters and pagination.
```sh
scadalink --contact-points <uri> deploy status [--instance-id <int>] [--status <string>] [--page <int>] [--page-size <int>]
scadalink --url <url> deploy status [--instance-id <int>] [--status <string>] [--page <int>] [--page-size <int>]
```
| Option | Required | Default | Description |
@@ -610,7 +594,7 @@ scadalink --contact-points <uri> deploy status [--instance-id <int>] [--status <
Get a single data connection by ID.
```sh
scadalink --contact-points <uri> data-connection get --id <int>
scadalink --url <url> data-connection get --id <int>
```
| Option | Required | Description |
@@ -622,7 +606,7 @@ scadalink --contact-points <uri> data-connection get --id <int>
List all configured data connections.
```sh
scadalink --contact-points <uri> data-connection list
scadalink --url <url> data-connection list
```
#### `data-connection create`
@@ -630,7 +614,7 @@ scadalink --contact-points <uri> data-connection list
Create a new data connection definition.
```sh
scadalink --contact-points <uri> data-connection create --name <string> --protocol <string> [--configuration <json>]
scadalink --url <url> data-connection create --name <string> --protocol <string> [--configuration <json>]
```
| Option | Required | Description |
@@ -644,7 +628,7 @@ scadalink --contact-points <uri> data-connection create --name <string> --protoc
Update a data connection definition.
```sh
scadalink --contact-points <uri> data-connection update --id <int> [--name <string>] [--protocol <string>] [--configuration <json>]
scadalink --url <url> data-connection update --id <int> [--name <string>] [--protocol <string>] [--configuration <json>]
```
| Option | Required | Description |
@@ -659,7 +643,7 @@ scadalink --contact-points <uri> data-connection update --id <int> [--name <stri
Delete a data connection.
```sh
scadalink --contact-points <uri> data-connection delete --id <int>
scadalink --url <url> data-connection delete --id <int>
```
| Option | Required | Description |
@@ -671,7 +655,7 @@ scadalink --contact-points <uri> data-connection delete --id <int>
Assign a data connection to a site.
```sh
scadalink --contact-points <uri> data-connection assign --connection-id <int> --site-id <int>
scadalink --url <url> data-connection assign --connection-id <int> --site-id <int>
```
| Option | Required | Description |
@@ -684,7 +668,7 @@ scadalink --contact-points <uri> data-connection assign --connection-id <int> --
Remove a data connection assignment from a site.
```sh
scadalink --contact-points <uri> data-connection unassign --connection-id <int> --site-id <int>
scadalink --url <url> data-connection unassign --connection-id <int> --site-id <int>
```
| Option | Required | Description |
@@ -701,7 +685,7 @@ scadalink --contact-points <uri> data-connection unassign --connection-id <int>
Get a single external system definition by ID.
```sh
scadalink --contact-points <uri> external-system get --id <int>
scadalink --url <url> external-system get --id <int>
```
| Option | Required | Description |
@@ -713,7 +697,7 @@ scadalink --contact-points <uri> external-system get --id <int>
List all external system definitions.
```sh
scadalink --contact-points <uri> external-system list
scadalink --url <url> external-system list
```
#### `external-system create`
@@ -721,7 +705,7 @@ scadalink --contact-points <uri> external-system list
Register an external HTTP system that scripts can call.
```sh
scadalink --contact-points <uri> external-system create --name <string> --endpoint-url <url> --auth-type <string> [--auth-config <json>]
scadalink --url <url> external-system create --name <string> --endpoint-url <url> --auth-type <string> [--auth-config <json>]
```
| Option | Required | Description |
@@ -736,7 +720,7 @@ scadalink --contact-points <uri> external-system create --name <string> --endpoi
Update an external system definition.
```sh
scadalink --contact-points <uri> external-system update --id <int> [--name <string>] [--endpoint-url <url>] [--auth-type <string>] [--auth-config <json>]
scadalink --url <url> external-system update --id <int> [--name <string>] [--endpoint-url <url>] [--auth-type <string>] [--auth-config <json>]
```
| Option | Required | Description |
@@ -752,7 +736,7 @@ scadalink --contact-points <uri> external-system update --id <int> [--name <stri
Delete an external system definition.
```sh
scadalink --contact-points <uri> external-system delete --id <int>
scadalink --url <url> external-system delete --id <int>
```
| Option | Required | Description |
@@ -768,7 +752,7 @@ scadalink --contact-points <uri> external-system delete --id <int>
Get a single notification list by ID.
```sh
scadalink --contact-points <uri> notification get --id <int>
scadalink --url <url> notification get --id <int>
```
| Option | Required | Description |
@@ -780,7 +764,7 @@ scadalink --contact-points <uri> notification get --id <int>
List all notification lists.
```sh
scadalink --contact-points <uri> notification list
scadalink --url <url> notification list
```
#### `notification create`
@@ -788,7 +772,7 @@ scadalink --contact-points <uri> notification list
Create a notification list with one or more recipients.
```sh
scadalink --contact-points <uri> notification create --name <string> --emails <email1,email2,...>
scadalink --url <url> notification create --name <string> --emails <email1,email2,...>
```
| Option | Required | Description |
@@ -801,7 +785,7 @@ scadalink --contact-points <uri> notification create --name <string> --emails <e
Update a notification list's name or recipients.
```sh
scadalink --contact-points <uri> notification update --id <int> [--name <string>] [--emails <email1,email2,...>]
scadalink --url <url> notification update --id <int> [--name <string>] [--emails <email1,email2,...>]
```
| Option | Required | Description |
@@ -815,7 +799,7 @@ scadalink --contact-points <uri> notification update --id <int> [--name <string>
Delete a notification list.
```sh
scadalink --contact-points <uri> notification delete --id <int>
scadalink --url <url> notification delete --id <int>
```
| Option | Required | Description |
@@ -827,7 +811,7 @@ scadalink --contact-points <uri> notification delete --id <int>
Show the current SMTP configuration.
```sh
scadalink --contact-points <uri> notification smtp list
scadalink --url <url> notification smtp list
```
#### `notification smtp update`
@@ -835,7 +819,7 @@ scadalink --contact-points <uri> notification smtp list
Update the SMTP configuration.
```sh
scadalink --contact-points <uri> notification smtp update --host <string> --port <int> --auth-type <string> [--username <string>] [--password <string>] [--from-address <string>]
scadalink --url <url> notification smtp update --host <string> --port <int> --auth-type <string> [--username <string>] [--password <string>] [--from-address <string>]
```
| Option | Required | Description |
@@ -856,7 +840,7 @@ scadalink --contact-points <uri> notification smtp update --host <string> --port
List all inbound API keys.
```sh
scadalink --contact-points <uri> security api-key list
scadalink --url <url> security api-key list
```
#### `security api-key create`
@@ -864,7 +848,7 @@ scadalink --contact-points <uri> security api-key list
Create a new inbound API key. The generated key value is returned in the response and not stored in plaintext — save it immediately.
```sh
scadalink --contact-points <uri> security api-key create --name <string>
scadalink --url <url> security api-key create --name <string>
```
| Option | Required | Description |
@@ -876,7 +860,7 @@ scadalink --contact-points <uri> security api-key create --name <string>
Update an API key's name or enabled status.
```sh
scadalink --contact-points <uri> security api-key update --id <int> [--name <string>] [--enabled <bool>]
scadalink --url <url> security api-key update --id <int> [--name <string>] [--enabled <bool>]
```
| Option | Required | Description |
@@ -890,7 +874,7 @@ scadalink --contact-points <uri> security api-key update --id <int> [--name <str
Revoke and delete an API key.
```sh
scadalink --contact-points <uri> security api-key delete --id <int>
scadalink --url <url> security api-key delete --id <int>
```
| Option | Required | Description |
@@ -902,7 +886,7 @@ scadalink --contact-points <uri> security api-key delete --id <int>
List all LDAP group → role mappings.
```sh
scadalink --contact-points <uri> security role-mapping list
scadalink --url <url> security role-mapping list
```
#### `security role-mapping create`
@@ -910,7 +894,7 @@ scadalink --contact-points <uri> security role-mapping list
Map an LDAP group to a ScadaLink role.
```sh
scadalink --contact-points <uri> security role-mapping create --ldap-group <string> --role <string>
scadalink --url <url> security role-mapping create --ldap-group <string> --role <string>
```
| Option | Required | Description |
@@ -923,7 +907,7 @@ scadalink --contact-points <uri> security role-mapping create --ldap-group <stri
Update an LDAP role mapping.
```sh
scadalink --contact-points <uri> security role-mapping update --id <int> [--ldap-group <string>] [--role <string>]
scadalink --url <url> security role-mapping update --id <int> [--ldap-group <string>] [--role <string>]
```
| Option | Required | Description |
@@ -937,7 +921,7 @@ scadalink --contact-points <uri> security role-mapping update --id <int> [--ldap
Remove an LDAP role mapping.
```sh
scadalink --contact-points <uri> security role-mapping delete --id <int>
scadalink --url <url> security role-mapping delete --id <int>
```
| Option | Required | Description |
@@ -949,7 +933,7 @@ scadalink --contact-points <uri> security role-mapping delete --id <int>
List all site scope rules for role mappings.
```sh
scadalink --contact-points <uri> security scope-rule list [--role-mapping-id <int>]
scadalink --url <url> security scope-rule list [--role-mapping-id <int>]
```
| Option | Required | Description |
@@ -961,7 +945,7 @@ scadalink --contact-points <uri> security scope-rule list [--role-mapping-id <in
Add a site scope rule to a role mapping, restricting it to a specific site.
```sh
scadalink --contact-points <uri> security scope-rule add --role-mapping-id <int> --site-id <int>
scadalink --url <url> security scope-rule add --role-mapping-id <int> --site-id <int>
```
| Option | Required | Description |
@@ -974,7 +958,7 @@ scadalink --contact-points <uri> security scope-rule add --role-mapping-id <int>
Remove a site scope rule from a role mapping.
```sh
scadalink --contact-points <uri> security scope-rule delete --id <int>
scadalink --url <url> security scope-rule delete --id <int>
```
| Option | Required | Description |
@@ -990,7 +974,7 @@ scadalink --contact-points <uri> security scope-rule delete --id <int>
Return the current health state for all known sites as a JSON object keyed by site identifier.
```sh
scadalink --contact-points <uri> health summary
scadalink --url <url> health summary
```
#### `health site`
@@ -998,7 +982,7 @@ scadalink --contact-points <uri> health summary
Return the health state for a single site.
```sh
scadalink --contact-points <uri> health site --identifier <string>
scadalink --url <url> health site --identifier <string>
```
| Option | Required | Description |
@@ -1010,7 +994,7 @@ scadalink --contact-points <uri> health site --identifier <string>
Query the site event log for a specific site. Events are fetched remotely from the site's local SQLite store.
```sh
scadalink --contact-points <uri> health event-log --site-identifier <string> [--from <datetime>] [--to <datetime>] [--search <string>] [--page <int>] [--page-size <int>]
scadalink --url <url> health event-log --site-identifier <string> [--from <datetime>] [--to <datetime>] [--search <string>] [--page <int>] [--page-size <int>]
```
| Option | Required | Default | Description |
@@ -1027,7 +1011,7 @@ scadalink --contact-points <uri> health event-log --site-identifier <string> [--
Query parked (dead-letter) messages at a specific site.
```sh
scadalink --contact-points <uri> health parked-messages --site-identifier <string> [--page <int>] [--page-size <int>]
scadalink --url <url> health parked-messages --site-identifier <string> [--page <int>] [--page-size <int>]
```
| Option | Required | Default | Description |
@@ -1045,7 +1029,7 @@ scadalink --contact-points <uri> health parked-messages --site-identifier <strin
Request a point-in-time snapshot of a running instance's current attribute values and alarm states from the site. The instance must be deployed and enabled.
```sh
scadalink --contact-points <uri> debug snapshot --id <int>
scadalink --url <url> debug snapshot --id <int>
```
| Option | Required | Description |
@@ -1063,7 +1047,7 @@ The command resolves the instance's site internally and routes the request to th
Query the central audit log with optional filters and pagination.
```sh
scadalink --contact-points <uri> audit-log query [options]
scadalink --url <url> audit-log query [options]
```
| Option | Required | Default | Description |
@@ -1085,7 +1069,7 @@ scadalink --contact-points <uri> audit-log query [options]
List all shared script definitions.
```sh
scadalink --contact-points <uri> shared-script list
scadalink --url <url> shared-script list
```
#### `shared-script get`
@@ -1093,7 +1077,7 @@ scadalink --contact-points <uri> shared-script list
Get a single shared script by ID.
```sh
scadalink --contact-points <uri> shared-script get --id <int>
scadalink --url <url> shared-script get --id <int>
```
| Option | Required | Description |
@@ -1105,7 +1089,7 @@ scadalink --contact-points <uri> shared-script get --id <int>
Create a new shared script.
```sh
scadalink --contact-points <uri> shared-script create --name <string> --code <string>
scadalink --url <url> shared-script create --name <string> --code <string>
```
| Option | Required | Description |
@@ -1118,7 +1102,7 @@ scadalink --contact-points <uri> shared-script create --name <string> --code <st
Update a shared script's name or code.
```sh
scadalink --contact-points <uri> shared-script update --id <int> [--name <string>] [--code <string>]
scadalink --url <url> shared-script update --id <int> [--name <string>] [--code <string>]
```
| Option | Required | Description |
@@ -1132,7 +1116,7 @@ scadalink --contact-points <uri> shared-script update --id <int> [--name <string
Delete a shared script.
```sh
scadalink --contact-points <uri> shared-script delete --id <int>
scadalink --url <url> shared-script delete --id <int>
```
| Option | Required | Description |
@@ -1148,7 +1132,7 @@ scadalink --contact-points <uri> shared-script delete --id <int>
List all database connection definitions.
```sh
scadalink --contact-points <uri> db-connection list
scadalink --url <url> db-connection list
```
#### `db-connection get`
@@ -1156,7 +1140,7 @@ scadalink --contact-points <uri> db-connection list
Get a single database connection by ID.
```sh
scadalink --contact-points <uri> db-connection get --id <int>
scadalink --url <url> db-connection get --id <int>
```
| Option | Required | Description |
@@ -1168,7 +1152,7 @@ scadalink --contact-points <uri> db-connection get --id <int>
Create a new database connection definition.
```sh
scadalink --contact-points <uri> db-connection create --name <string> --connection-string <string> [--provider <string>]
scadalink --url <url> db-connection create --name <string> --connection-string <string> [--provider <string>]
```
| Option | Required | Description |
@@ -1182,7 +1166,7 @@ scadalink --contact-points <uri> db-connection create --name <string> --connecti
Update a database connection definition.
```sh
scadalink --contact-points <uri> db-connection update --id <int> [--name <string>] [--connection-string <string>] [--provider <string>]
scadalink --url <url> db-connection update --id <int> [--name <string>] [--connection-string <string>] [--provider <string>]
```
| Option | Required | Description |
@@ -1197,7 +1181,7 @@ scadalink --contact-points <uri> db-connection update --id <int> [--name <string
Delete a database connection definition.
```sh
scadalink --contact-points <uri> db-connection delete --id <int>
scadalink --url <url> db-connection delete --id <int>
```
| Option | Required | Description |
@@ -1213,7 +1197,7 @@ scadalink --contact-points <uri> db-connection delete --id <int>
List all inbound API method definitions.
```sh
scadalink --contact-points <uri> api-method list
scadalink --url <url> api-method list
```
#### `api-method get`
@@ -1221,7 +1205,7 @@ scadalink --contact-points <uri> api-method list
Get a single inbound API method by ID.
```sh
scadalink --contact-points <uri> api-method get --id <int>
scadalink --url <url> api-method get --id <int>
```
| Option | Required | Description |
@@ -1233,7 +1217,7 @@ scadalink --contact-points <uri> api-method get --id <int>
Create a new inbound API method.
```sh
scadalink --contact-points <uri> api-method create --name <string> --code <string> [--description <string>]
scadalink --url <url> api-method create --name <string> --code <string> [--description <string>]
```
| Option | Required | Description |
@@ -1247,7 +1231,7 @@ scadalink --contact-points <uri> api-method create --name <string> --code <strin
Update an inbound API method.
```sh
scadalink --contact-points <uri> api-method update --id <int> [--name <string>] [--code <string>] [--description <string>]
scadalink --url <url> api-method update --id <int> [--name <string>] [--code <string>] [--description <string>]
```
| Option | Required | Description |
@@ -1262,7 +1246,7 @@ scadalink --contact-points <uri> api-method update --id <int> [--name <string>]
Delete an inbound API method.
```sh
scadalink --contact-points <uri> api-method delete --id <int>
scadalink --url <url> api-method delete --id <int>
```
| Option | Required | Description |

View File

@@ -11,16 +11,9 @@
<InternalsVisibleTo Include="ScadaLink.CLI.Tests" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Akka" Version="1.5.62" />
<PackageReference Include="Akka.Remote" Version="1.5.62" />
<PackageReference Include="Akka.Cluster.Tools" Version="1.5.62" />
<PackageReference Include="System.CommandLine" Version="2.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.5" />
<PackageReference Include="Novell.Directory.Ldap.NETStandard" Version="3.6.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../ScadaLink.Commons/ScadaLink.Commons.csproj" />
<ProjectReference Include="../ScadaLink.Security/ScadaLink.Security.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,36 @@
using System.Collections.Frozen;
namespace ScadaLink.Commons.Messages.Management;
public static class ManagementCommandRegistry
{
private static readonly FrozenDictionary<string, Type> Commands = BuildRegistry();
public static Type? Resolve(string commandName)
{
return Commands.GetValueOrDefault(commandName);
}
public static string GetCommandName(Type commandType)
{
var name = commandType.Name;
return name.EndsWith("Command", StringComparison.Ordinal)
? name[..^"Command".Length]
: name;
}
private static FrozenDictionary<string, Type> BuildRegistry()
{
var ns = typeof(ManagementEnvelope).Namespace!;
var assembly = typeof(ManagementEnvelope).Assembly;
return assembly.GetTypes()
.Where(t => t.Namespace == ns
&& t.Name.EndsWith("Command", StringComparison.Ordinal)
&& !t.IsAbstract)
.ToFrozenDictionary(
t => t.Name[..^"Command".Length],
t => t,
StringComparer.OrdinalIgnoreCase);
}
}

View File

@@ -483,11 +483,18 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
private void ReSubscribeAll()
{
_log.Info("[{0}] Re-subscribing {1} tags after reconnect", _connectionName, _subscriptionIds.Count);
// Derive tag list from _subscriptionsByInstance (durable source of truth),
// not _subscriptionIds which gets cleared and is only repopulated on success.
var allTags = _subscriptionsByInstance.Values
.SelectMany(tags => tags)
.Distinct()
.ToList();
_log.Info("[{0}] Re-subscribing {1} tags after reconnect", _connectionName, allTags.Count);
var self = Self;
var allTags = _subscriptionIds.Keys.ToList();
_subscriptionIds.Clear();
_unresolvedTags.Clear();
_resolvedTags = 0;
foreach (var tagPath in allTags)
@@ -535,6 +542,16 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
{
_log.Debug("[{0}] Tag resolution still failing for {1}: {2}",
_connectionName, msg.TagPath, msg.Error);
// Track as unresolved so periodic retry picks it up
if (_unresolvedTags.Add(msg.TagPath))
{
Timers.StartPeriodicTimer(
"tag-resolution-retry",
new RetryTagResolution(),
_options.TagResolutionRetryInterval,
_options.TagResolutionRetryInterval);
}
}
private void HandleTagValueReceived(TagValueReceived msg)

View File

@@ -192,6 +192,8 @@ akka {{
Props.Create(() => new ScadaLink.ManagementService.ManagementActor(_serviceProvider, mgmtLogger)),
"management");
ClusterClientReceptionist.Get(_actorSystem).RegisterService(mgmtActor);
var mgmtHolder = _serviceProvider.GetRequiredService<ScadaLink.ManagementService.ManagementActorHolder>();
mgmtHolder.ActorRef = mgmtActor;
_logger.LogInformation("ManagementActor registered with ClusterClientReceptionist");
_logger.LogInformation("Central actors registered. CentralCommunicationActor created.");

View File

@@ -129,6 +129,7 @@ try
app.MapStaticAssets();
app.MapCentralUI<ScadaLink.Host.Components.App>();
app.MapInboundAPI();
app.MapManagementAPI();
// Compile and register all Inbound API method scripts at startup
using (var scope = app.Services.CreateScope())

View File

@@ -0,0 +1,8 @@
using Akka.Actor;
namespace ScadaLink.ManagementService;
public class ManagementActorHolder
{
public IActorRef? ActorRef { get; set; }
}

View File

@@ -0,0 +1,151 @@
using System.Text;
using System.Text.Json;
using Akka.Actor;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Messages.Management;
using ScadaLink.Security;
namespace ScadaLink.ManagementService;
public static class ManagementEndpoints
{
private static readonly TimeSpan AskTimeout = TimeSpan.FromSeconds(30);
public static IEndpointRouteBuilder MapManagementAPI(this IEndpointRouteBuilder endpoints)
{
endpoints.MapPost("/management", (Delegate)HandleRequest);
return endpoints;
}
private static async Task<IResult> HandleRequest(HttpContext context)
{
var logger = context.RequestServices.GetRequiredService<ILogger<ManagementActorHolder>>();
// 1. Decode Basic Auth
var authHeader = context.Request.Headers.Authorization.ToString();
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Basic ", StringComparison.OrdinalIgnoreCase))
{
return Results.Json(new { error = "Authorization header required (Basic scheme).", code = "AUTH_FAILED" }, statusCode: 401);
}
string username, password;
try
{
var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(authHeader["Basic ".Length..]));
var colon = decoded.IndexOf(':');
if (colon < 0) throw new FormatException();
username = decoded[..colon];
password = decoded[(colon + 1)..];
}
catch
{
return Results.Json(new { error = "Malformed Basic Auth header.", code = "AUTH_FAILED" }, statusCode: 401);
}
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
{
return Results.Json(new { error = "Username and password are required.", code = "AUTH_FAILED" }, statusCode: 401);
}
// 2. LDAP authentication
var ldapAuth = context.RequestServices.GetRequiredService<LdapAuthService>();
var authResult = await ldapAuth.AuthenticateAsync(username, password);
if (!authResult.Success)
{
return Results.Json(
new { error = authResult.ErrorMessage ?? "Authentication failed.", code = "AUTH_FAILED" },
statusCode: 401);
}
// 3. Role resolution
var roleMapper = context.RequestServices.GetRequiredService<RoleMapper>();
var mappingResult = await roleMapper.MapGroupsToRolesAsync(
authResult.Groups ?? (IReadOnlyList<string>)Array.Empty<string>());
var permittedSiteIds = mappingResult.IsSystemWideDeployment
? Array.Empty<string>()
: mappingResult.PermittedSiteIds.ToArray();
var authenticatedUser = new AuthenticatedUser(
authResult.Username!,
authResult.DisplayName!,
mappingResult.Roles.ToArray(),
permittedSiteIds);
// 4. Parse command from request body
JsonDocument doc;
try
{
doc = await JsonDocument.ParseAsync(context.Request.Body);
}
catch
{
return Results.Json(new { error = "Invalid JSON body.", code = "BAD_REQUEST" }, statusCode: 400);
}
if (!doc.RootElement.TryGetProperty("command", out var commandNameElement))
{
return Results.Json(new { error = "Missing 'command' field.", code = "BAD_REQUEST" }, statusCode: 400);
}
var commandName = commandNameElement.GetString();
if (string.IsNullOrWhiteSpace(commandName))
{
return Results.Json(new { error = "Empty 'command' field.", code = "BAD_REQUEST" }, statusCode: 400);
}
var commandType = ManagementCommandRegistry.Resolve(commandName);
if (commandType == null)
{
return Results.Json(new { error = $"Unknown command: '{commandName}'.", code = "BAD_REQUEST" }, statusCode: 400);
}
object command;
try
{
var payloadElement = doc.RootElement.TryGetProperty("payload", out var p)
? p
: JsonDocument.Parse("{}").RootElement;
command = JsonSerializer.Deserialize(payloadElement.GetRawText(), commandType,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!;
}
catch (Exception ex)
{
return Results.Json(new { error = $"Failed to deserialize payload: {ex.Message}", code = "BAD_REQUEST" }, statusCode: 400);
}
// 5. Dispatch to ManagementActor
var holder = context.RequestServices.GetRequiredService<ManagementActorHolder>();
if (holder.ActorRef == null)
{
return Results.Json(new { error = "Management service not ready.", code = "SERVICE_UNAVAILABLE" }, statusCode: 503);
}
var correlationId = Guid.NewGuid().ToString("N");
var envelope = new ManagementEnvelope(authenticatedUser, command, correlationId);
object response;
try
{
response = await holder.ActorRef.Ask(envelope, AskTimeout);
}
catch (Exception ex)
{
logger.LogError(ex, "ManagementActor Ask timed out or failed (CorrelationId={CorrelationId})", correlationId);
return Results.Json(new { error = "Request timed out.", code = "TIMEOUT" }, statusCode: 504);
}
// 6. Map response
return response switch
{
ManagementSuccess success => Results.Text(success.JsonData, "application/json", statusCode: 200),
ManagementError error => Results.Json(new { error = error.Error, code = error.ErrorCode }, statusCode: 400),
ManagementUnauthorized unauth => Results.Json(new { error = unauth.Message, code = "UNAUTHORIZED" }, statusCode: 403),
_ => Results.Json(new { error = "Unexpected response.", code = "INTERNAL_ERROR" }, statusCode: 500)
};
}
}

View File

@@ -5,12 +5,12 @@
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Akka" Version="1.5.62" />
<PackageReference Include="Akka.Cluster.Tools" Version="1.5.62" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../ScadaLink.Commons/ScadaLink.Commons.csproj" />

View File

@@ -6,6 +6,7 @@ public static class ServiceCollectionExtensions
{
public static IServiceCollection AddManagementService(this IServiceCollection services)
{
services.AddSingleton<ManagementActorHolder>();
services.AddOptions<ManagementServiceOptions>()
.BindConfiguration("ScadaLink:ManagementService");
return services;

View File

@@ -5,68 +5,43 @@ namespace ScadaLink.CLI.Tests;
public class CliConfigTests
{
[Fact]
public void Load_DefaultValues_WhenNoConfigExists()
public void Load_DefaultFormat_IsJson()
{
// Clear environment variables that might affect the test
var origContact = Environment.GetEnvironmentVariable("SCADALINK_CONTACT_POINTS");
var origLdap = Environment.GetEnvironmentVariable("SCADALINK_LDAP_SERVER");
var origUrl = Environment.GetEnvironmentVariable("SCADALINK_MANAGEMENT_URL");
var origFormat = Environment.GetEnvironmentVariable("SCADALINK_FORMAT");
try
{
Environment.SetEnvironmentVariable("SCADALINK_CONTACT_POINTS", null);
Environment.SetEnvironmentVariable("SCADALINK_LDAP_SERVER", null);
Environment.SetEnvironmentVariable("SCADALINK_MANAGEMENT_URL", null);
Environment.SetEnvironmentVariable("SCADALINK_FORMAT", null);
var config = CliConfig.Load();
Assert.Equal(636, config.LdapPort);
Assert.True(config.LdapUseTls);
// DefaultFormat is always "json" unless overridden by config file or env var
Assert.Equal("json", config.DefaultFormat);
}
finally
{
Environment.SetEnvironmentVariable("SCADALINK_CONTACT_POINTS", origContact);
Environment.SetEnvironmentVariable("SCADALINK_LDAP_SERVER", origLdap);
Environment.SetEnvironmentVariable("SCADALINK_MANAGEMENT_URL", origUrl);
Environment.SetEnvironmentVariable("SCADALINK_FORMAT", origFormat);
}
}
[Fact]
public void Load_ContactPoints_FromEnvironment()
public void Load_ManagementUrl_FromEnvironment()
{
var orig = Environment.GetEnvironmentVariable("SCADALINK_CONTACT_POINTS");
var orig = Environment.GetEnvironmentVariable("SCADALINK_MANAGEMENT_URL");
try
{
Environment.SetEnvironmentVariable("SCADALINK_CONTACT_POINTS", "host1:8080,host2:8080");
Environment.SetEnvironmentVariable("SCADALINK_MANAGEMENT_URL", "http://central:5000");
var config = CliConfig.Load();
Assert.Equal(2, config.ContactPoints.Count);
Assert.Equal("host1:8080", config.ContactPoints[0]);
Assert.Equal("host2:8080", config.ContactPoints[1]);
Assert.Equal("http://central:5000", config.ManagementUrl);
}
finally
{
Environment.SetEnvironmentVariable("SCADALINK_CONTACT_POINTS", orig);
}
}
[Fact]
public void Load_LdapServer_FromEnvironment()
{
var orig = Environment.GetEnvironmentVariable("SCADALINK_LDAP_SERVER");
try
{
Environment.SetEnvironmentVariable("SCADALINK_LDAP_SERVER", "ldap.example.com");
var config = CliConfig.Load();
Assert.Equal("ldap.example.com", config.LdapServer);
}
finally
{
Environment.SetEnvironmentVariable("SCADALINK_LDAP_SERVER", orig);
Environment.SetEnvironmentVariable("SCADALINK_MANAGEMENT_URL", orig);
}
}

View File

@@ -1,17 +1,17 @@
using ScadaLink.CLI;
using ScadaLink.CLI.Commands;
using ScadaLink.Commons.Messages.Management;
namespace ScadaLink.CLI.Tests;
public class CommandHelpersTests
{
[Fact]
public void HandleResponse_ManagementSuccess_JsonFormat_ReturnsZero()
public void HandleResponse_Success_JsonFormat_ReturnsZero()
{
var writer = new StringWriter();
Console.SetOut(writer);
var response = new ManagementSuccess("corr-1", "{\"id\":1,\"name\":\"test\"}");
var response = new ManagementResponse(200, "{\"id\":1,\"name\":\"test\"}", null, null);
var exitCode = CommandHelpers.HandleResponse(response, "json");
Assert.Equal(0, exitCode);
@@ -21,13 +21,13 @@ public class CommandHelpersTests
}
[Fact]
public void HandleResponse_ManagementSuccess_TableFormat_ArrayData_ReturnsZero()
public void HandleResponse_Success_TableFormat_ArrayData_ReturnsZero()
{
var writer = new StringWriter();
Console.SetOut(writer);
var json = "[{\"Id\":1,\"Name\":\"Alpha\"},{\"Id\":2,\"Name\":\"Beta\"}]";
var response = new ManagementSuccess("corr-1", json);
var response = new ManagementResponse(200, json, null, null);
var exitCode = CommandHelpers.HandleResponse(response, "table");
Assert.Equal(0, exitCode);
@@ -41,13 +41,13 @@ public class CommandHelpersTests
}
[Fact]
public void HandleResponse_ManagementSuccess_TableFormat_ObjectData_ReturnsZero()
public void HandleResponse_Success_TableFormat_ObjectData_ReturnsZero()
{
var writer = new StringWriter();
Console.SetOut(writer);
var json = "{\"Id\":1,\"Name\":\"Alpha\",\"Status\":\"Active\"}";
var response = new ManagementSuccess("corr-1", json);
var response = new ManagementResponse(200, json, null, null);
var exitCode = CommandHelpers.HandleResponse(response, "table");
Assert.Equal(0, exitCode);
@@ -61,12 +61,12 @@ public class CommandHelpersTests
}
[Fact]
public void HandleResponse_ManagementSuccess_TableFormat_EmptyArray_ShowsNoResults()
public void HandleResponse_Success_TableFormat_EmptyArray_ShowsNoResults()
{
var writer = new StringWriter();
Console.SetOut(writer);
var response = new ManagementSuccess("corr-1", "[]");
var response = new ManagementResponse(200, "[]", null, null);
var exitCode = CommandHelpers.HandleResponse(response, "table");
Assert.Equal(0, exitCode);
@@ -76,12 +76,12 @@ public class CommandHelpersTests
}
[Fact]
public void HandleResponse_ManagementError_ReturnsOne()
public void HandleResponse_Error_ReturnsOne()
{
var errWriter = new StringWriter();
Console.SetError(errWriter);
var response = new ManagementError("corr-1", "Something failed", "FAIL_CODE");
var response = new ManagementResponse(400, null, "Something failed", "FAIL_CODE");
var exitCode = CommandHelpers.HandleResponse(response, "json");
Assert.Equal(1, exitCode);
@@ -91,12 +91,12 @@ public class CommandHelpersTests
}
[Fact]
public void HandleResponse_ManagementUnauthorized_ReturnsTwo()
public void HandleResponse_Unauthorized_ReturnsTwo()
{
var errWriter = new StringWriter();
Console.SetError(errWriter);
var response = new ManagementUnauthorized("corr-1", "Access denied");
var response = new ManagementResponse(403, null, "Access denied", "UNAUTHORIZED");
var exitCode = CommandHelpers.HandleResponse(response, "json");
Assert.Equal(2, exitCode);
@@ -106,26 +106,47 @@ public class CommandHelpersTests
}
[Fact]
public void HandleResponse_UnexpectedType_ReturnsOne()
public void HandleResponse_AuthFailure_ReturnsOne()
{
var errWriter = new StringWriter();
Console.SetError(errWriter);
var exitCode = CommandHelpers.HandleResponse("unexpected", "json");
var response = new ManagementResponse(401, null, "Invalid credentials", "AUTH_FAILED");
var exitCode = CommandHelpers.HandleResponse(response, "json");
Assert.Equal(1, exitCode);
Assert.Contains("Unexpected response type", errWriter.ToString());
Assert.Contains("Invalid credentials", errWriter.ToString());
Console.SetError(new StreamWriter(Console.OpenStandardError()) { AutoFlush = true });
}
[Fact]
public void NewCorrelationId_ReturnsNonEmpty32CharHex()
public void HandleResponse_ConnectionFailure_ReturnsOne()
{
var id = CommandHelpers.NewCorrelationId();
var errWriter = new StringWriter();
Console.SetError(errWriter);
Assert.NotNull(id);
Assert.Equal(32, id.Length);
Assert.True(id.All(c => "0123456789abcdef".Contains(c)));
var response = new ManagementResponse(0, null, "Connection failed: No such host", "CONNECTION_FAILED");
var exitCode = CommandHelpers.HandleResponse(response, "json");
Assert.Equal(1, exitCode);
Assert.Contains("Connection failed", errWriter.ToString());
Console.SetError(new StreamWriter(Console.OpenStandardError()) { AutoFlush = true });
}
[Fact]
public void HandleResponse_Timeout_ReturnsOne()
{
var errWriter = new StringWriter();
Console.SetError(errWriter);
var response = new ManagementResponse(504, null, "Request timed out.", "TIMEOUT");
var exitCode = CommandHelpers.HandleResponse(response, "json");
Assert.Equal(1, exitCode);
Assert.Contains("timed out", errWriter.ToString());
Console.SetError(new StreamWriter(Console.OpenStandardError()) { AutoFlush = true });
}
}

View File

@@ -157,6 +157,8 @@ public class ArchitecturalConstraintTests
.Where(t => t.Namespace != null
&& t.Namespace.Contains(".Messages.")
&& !t.IsEnum && !t.IsInterface
&& !(t.IsAbstract && t.IsSealed) // exclude static classes (utilities)
&& !t.Name.StartsWith("<") // exclude compiler-generated types
&& (t.IsClass || (t.IsValueType && !t.IsPrimitive)));
foreach (var type in messageTypes)

View File

@@ -13,6 +13,8 @@ public class MessageConventionTests
&& t.Namespace.Contains(".Messages.")
&& !t.IsEnum
&& !t.IsInterface
&& !(t.IsAbstract && t.IsSealed) // exclude static classes (utilities)
&& !t.Name.StartsWith("<") // exclude compiler-generated types
&& (t.IsClass || (t.IsValueType && !t.IsPrimitive)));
[Fact]