diff --git a/CLAUDE.md b/CLAUDE.md index f65d651..f5839e3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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). diff --git a/Component-CLI.md b/Component-CLI.md index eebf2a5..3ef97a3 100644 --- a/Component-CLI.md +++ b/Component-CLI.md @@ -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 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. diff --git a/Component-ManagementService.md b/Component-ManagementService.md index 0a8521b..20f5594 100644 --- a/Component-ManagementService.md +++ b/Component-ManagementService.md @@ -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. diff --git a/HighLevelReqs.md b/HighLevelReqs.md index ead20a1..3694966 100644 --- a/HighLevelReqs.md +++ b/HighLevelReqs.md @@ -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 diff --git a/docker/README.md b/docker/README.md index 1d998bc..4cb9de3 100644 --- a/docker/README.md +++ b/docker/README.md @@ -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 -- \ diff --git a/src/ScadaLink.CLI/CliConfig.cs b/src/ScadaLink.CLI/CliConfig.cs index d0f7f9d..80d775e 100644 --- a/src/ScadaLink.CLI/CliConfig.cs +++ b/src/ScadaLink.CLI/CliConfig.cs @@ -4,13 +4,7 @@ namespace ScadaLink.CLI; public class CliConfig { - public List 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? 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; } - } } diff --git a/src/ScadaLink.CLI/ClusterConnection.cs b/src/ScadaLink.CLI/ClusterConnection.cs deleted file mode 100644 index 89ab021..0000000 --- a/src/ScadaLink.CLI/ClusterConnection.cs +++ /dev/null @@ -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 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 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; - } - } -} diff --git a/src/ScadaLink.CLI/Commands/ApiMethodCommands.cs b/src/ScadaLink.CLI/Commands/ApiMethodCommands.cs index 18eccdf..cde69a6 100644 --- a/src/ScadaLink.CLI/Commands/ApiMethodCommands.cs +++ b/src/ScadaLink.CLI/Commands/ApiMethodCommands.cs @@ -6,31 +6,31 @@ namespace ScadaLink.CLI.Commands; public static class ApiMethodCommands { - public static Command Build(Option contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildList(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildGet(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildCreate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var nameOption = new Option("--name") { Description = "Method name", Required = true }; var scriptOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildUpdate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--id") { Description = "API method ID", Required = true }; var scriptOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildDelete(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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; } diff --git a/src/ScadaLink.CLI/Commands/AuditLogCommands.cs b/src/ScadaLink.CLI/Commands/AuditLogCommands.cs index 78d4437..00fc084 100644 --- a/src/ScadaLink.CLI/Commands/AuditLogCommands.cs +++ b/src/ScadaLink.CLI/Commands/AuditLogCommands.cs @@ -6,16 +6,16 @@ namespace ScadaLink.CLI.Commands; public static class AuditLogCommands { - public static Command Build(Option contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildQuery(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var userOption = new Option("--user") { Description = "Filter by username" }; var entityTypeOption = new Option("--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; diff --git a/src/ScadaLink.CLI/Commands/CommandHelpers.cs b/src/ScadaLink.CLI/Commands/CommandHelpers.cs index 1da0676..d424fc6 100644 --- a/src/ScadaLink.CLI/Commands/CommandHelpers.cs +++ b/src/ScadaLink.CLI/Commands/CommandHelpers.cs @@ -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 ExecuteCommandAsync( ParseResult result, - Option contactPointsOption, + Option urlOption, Option formatOption, Option usernameOption, Option 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.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(), Array.Empty()), - new ResolveRolesCommand(authResult.Groups ?? (IReadOnlyList)Array.Empty()), - 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(); - permittedSiteIds = rolesDoc.RootElement.TryGetProperty("PermittedSiteIds", out var sitesEl) - ? sitesEl.EnumerateArray().Select(e => e.GetString()!).ToArray() - : Array.Empty(); - } - 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() }); diff --git a/src/ScadaLink.CLI/Commands/DataConnectionCommands.cs b/src/ScadaLink.CLI/Commands/DataConnectionCommands.cs index e2c58ba..2ea93fb 100644 --- a/src/ScadaLink.CLI/Commands/DataConnectionCommands.cs +++ b/src/ScadaLink.CLI/Commands/DataConnectionCommands.cs @@ -6,22 +6,22 @@ namespace ScadaLink.CLI.Commands; public static class DataConnectionCommands { - public static Command Build(Option contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildGet(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildUpdate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--id") { Description = "Data connection ID", Required = true }; var nameOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildUnassign(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildList(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildCreate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var nameOption = new Option("--name") { Description = "Connection name", Required = true }; var protocolOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildDelete(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildAssign(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var connectionIdOption = new Option("--connection-id") { Description = "Data connection ID", Required = true }; var siteIdOption = new Option("--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; diff --git a/src/ScadaLink.CLI/Commands/DbConnectionCommands.cs b/src/ScadaLink.CLI/Commands/DbConnectionCommands.cs index a606ec7..4bbe234 100644 --- a/src/ScadaLink.CLI/Commands/DbConnectionCommands.cs +++ b/src/ScadaLink.CLI/Commands/DbConnectionCommands.cs @@ -6,31 +6,31 @@ namespace ScadaLink.CLI.Commands; public static class DbConnectionCommands { - public static Command Build(Option contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildList(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildGet(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildCreate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var nameOption = new Option("--name") { Description = "Connection name", Required = true }; var connStrOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildUpdate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--id") { Description = "Database connection ID", Required = true }; var nameOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildDelete(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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; } diff --git a/src/ScadaLink.CLI/Commands/DebugCommands.cs b/src/ScadaLink.CLI/Commands/DebugCommands.cs index bd906ae..52df20b 100644 --- a/src/ScadaLink.CLI/Commands/DebugCommands.cs +++ b/src/ScadaLink.CLI/Commands/DebugCommands.cs @@ -6,16 +6,16 @@ namespace ScadaLink.CLI.Commands; public static class DebugCommands { - public static Command Build(Option contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildSnapshot(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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; diff --git a/src/ScadaLink.CLI/Commands/DeployCommands.cs b/src/ScadaLink.CLI/Commands/DeployCommands.cs index 51009ea..5957aac 100644 --- a/src/ScadaLink.CLI/Commands/DeployCommands.cs +++ b/src/ScadaLink.CLI/Commands/DeployCommands.cs @@ -6,18 +6,18 @@ namespace ScadaLink.CLI.Commands; public static class DeployCommands { - public static Command Build(Option contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildInstance(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildArtifacts(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var siteIdOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildStatus(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var instanceIdOption = new Option("--instance-id") { Description = "Filter by instance ID" }; var statusOption = new Option("--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; diff --git a/src/ScadaLink.CLI/Commands/ExternalSystemCommands.cs b/src/ScadaLink.CLI/Commands/ExternalSystemCommands.cs index c554aad..30a5895 100644 --- a/src/ScadaLink.CLI/Commands/ExternalSystemCommands.cs +++ b/src/ScadaLink.CLI/Commands/ExternalSystemCommands.cs @@ -6,21 +6,21 @@ namespace ScadaLink.CLI.Commands; public static class ExternalSystemCommands { - public static Command Build(Option contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildGet(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildUpdate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--id") { Description = "External system ID", Required = true }; var nameOption = new Option("--name") { Description = "System name", Required = true }; - var urlOption = new Option("--endpoint-url") { Description = "Endpoint URL", Required = true }; + var endpointUrlOption = new Option("--endpoint-url") { Description = "Endpoint URL", Required = true }; var authTypeOption = new Option("--auth-type") { Description = "Auth type", Required = true }; var authConfigOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildList(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildCreate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var nameOption = new Option("--name") { Description = "System name", Required = true }; - var urlOption = new Option("--endpoint-url") { Description = "Endpoint URL", Required = true }; + var endpointUrlOption = new Option("--endpoint-url") { Description = "Endpoint URL", Required = true }; var authTypeOption = new Option("--auth-type") { Description = "Auth type (ApiKey, BasicAuth)", Required = true }; var authConfigOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildDelete(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildMethodGroup(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildMethodList(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var sysIdOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildMethodGet(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildMethodCreate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var sysIdOption = new Option("--external-system-id") { Description = "External system ID", Required = true }; var nameOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildMethodUpdate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--id") { Description = "Method ID", Required = true }; var nameOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildMethodDelete(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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; diff --git a/src/ScadaLink.CLI/Commands/HealthCommands.cs b/src/ScadaLink.CLI/Commands/HealthCommands.cs index abb0ef4..f3ac6e4 100644 --- a/src/ScadaLink.CLI/Commands/HealthCommands.cs +++ b/src/ScadaLink.CLI/Commands/HealthCommands.cs @@ -6,30 +6,30 @@ namespace ScadaLink.CLI.Commands; public static class HealthCommands { - public static Command Build(Option contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildSummary(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildSite(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var identifierOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildEventLog(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var siteOption = new Option("--site") { Description = "Site identifier", Required = true }; var eventTypeOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildParkedMessages(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var siteOption = new Option("--site") { Description = "Site identifier", Required = true }; var pageOption = new Option("--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), diff --git a/src/ScadaLink.CLI/Commands/InstanceCommands.cs b/src/ScadaLink.CLI/Commands/InstanceCommands.cs index 1ec2d02..a039e89 100644 --- a/src/ScadaLink.CLI/Commands/InstanceCommands.cs +++ b/src/ScadaLink.CLI/Commands/InstanceCommands.cs @@ -6,26 +6,26 @@ namespace ScadaLink.CLI.Commands; public static class InstanceCommands { - public static Command Build(Option contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildGet(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildSetBindings(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--id") { Description = "Instance ID", Required = true }; var bindingsOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildList(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var siteIdOption = new Option("--site-id") { Description = "Filter by site ID" }; var templateIdOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildCreate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var nameOption = new Option("--name") { Description = "Unique instance name", Required = true }; var templateIdOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildDeploy(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildEnable(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildDisable(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildDelete(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildSetOverrides(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--id") { Description = "Instance ID", Required = true }; var overridesOption = new Option("--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>(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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildSetArea(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--id") { Description = "Instance ID", Required = true }; var areaIdOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildDiff(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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; diff --git a/src/ScadaLink.CLI/Commands/NotificationCommands.cs b/src/ScadaLink.CLI/Commands/NotificationCommands.cs index 474417a..82b1d8f 100644 --- a/src/ScadaLink.CLI/Commands/NotificationCommands.cs +++ b/src/ScadaLink.CLI/Commands/NotificationCommands.cs @@ -6,21 +6,21 @@ namespace ScadaLink.CLI.Commands; public static class NotificationCommands { - public static Command Build(Option contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildGet(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildUpdate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--id") { Description = "Notification list ID", Required = true }; var nameOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildSmtp(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildList(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildCreate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var nameOption = new Option("--name") { Description = "Notification list name", Required = true }; var emailsOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildDelete(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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; } diff --git a/src/ScadaLink.CLI/Commands/SecurityCommands.cs b/src/ScadaLink.CLI/Commands/SecurityCommands.cs index 64499aa..deea7e6 100644 --- a/src/ScadaLink.CLI/Commands/SecurityCommands.cs +++ b/src/ScadaLink.CLI/Commands/SecurityCommands.cs @@ -6,18 +6,18 @@ namespace ScadaLink.CLI.Commands; public static class SecurityCommands { - public static Command Build(Option contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildApiKey(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildRoleMapping(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildScopeRule(Option urlOption, Option formatOption, Option usernameOption, Option 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); diff --git a/src/ScadaLink.CLI/Commands/SharedScriptCommands.cs b/src/ScadaLink.CLI/Commands/SharedScriptCommands.cs index 182ced5..e64767a 100644 --- a/src/ScadaLink.CLI/Commands/SharedScriptCommands.cs +++ b/src/ScadaLink.CLI/Commands/SharedScriptCommands.cs @@ -6,31 +6,31 @@ namespace ScadaLink.CLI.Commands; public static class SharedScriptCommands { - public static Command Build(Option contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildList(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildGet(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildCreate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var nameOption = new Option("--name") { Description = "Script name", Required = true }; var codeOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildUpdate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--id") { Description = "Shared script ID", Required = true }; var nameOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildDelete(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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; } diff --git a/src/ScadaLink.CLI/Commands/SiteCommands.cs b/src/ScadaLink.CLI/Commands/SiteCommands.cs index 7435f59..af9f690 100644 --- a/src/ScadaLink.CLI/Commands/SiteCommands.cs +++ b/src/ScadaLink.CLI/Commands/SiteCommands.cs @@ -6,22 +6,22 @@ namespace ScadaLink.CLI.Commands; public static class SiteCommands { - public static Command Build(Option contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildGet(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildList(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildCreate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var nameOption = new Option("--name") { Description = "Site name", Required = true }; var identifierOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildUpdate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--id") { Description = "Site ID", Required = true }; var nameOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildDelete(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildArea(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildDeployArtifacts(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var siteIdOption = new Option("--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; } diff --git a/src/ScadaLink.CLI/Commands/TemplateCommands.cs b/src/ScadaLink.CLI/Commands/TemplateCommands.cs index 9b221f6..dd9a4cd 100644 --- a/src/ScadaLink.CLI/Commands/TemplateCommands.cs +++ b/src/ScadaLink.CLI/Commands/TemplateCommands.cs @@ -6,36 +6,36 @@ namespace ScadaLink.CLI.Commands; public static class TemplateCommands { - public static Command Build(Option contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + public static Command Build(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildList(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildGet(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildCreate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var nameOption = new Option("--name") { Description = "Template name", Required = true }; var descOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildUpdate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--id") { Description = "Template ID", Required = true }; var nameOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildValidate(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildDelete(Option urlOption, Option formatOption, Option usernameOption, Option passwordOption) { var idOption = new Option("--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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildAttribute(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildAlarm(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildScript(Option urlOption, Option formatOption, Option usernameOption, Option 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 contactPointsOption, Option formatOption, Option usernameOption, Option passwordOption) + private static Command BuildComposition(Option urlOption, Option formatOption, Option usernameOption, Option 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); diff --git a/src/ScadaLink.CLI/ManagementHttpClient.cs b/src/ScadaLink.CLI/ManagementHttpClient.cs new file mode 100644 index 0000000..3026024 --- /dev/null +++ b/src/ScadaLink.CLI/ManagementHttpClient.cs @@ -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 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); diff --git a/src/ScadaLink.CLI/Program.cs b/src/ScadaLink.CLI/Program.cs index fef2c00..0d47849 100644 --- a/src/ScadaLink.CLI/Program.cs +++ b/src/ScadaLink.CLI/Program.cs @@ -4,32 +4,32 @@ using ScadaLink.CLI.Commands; var rootCommand = new RootCommand("ScadaLink CLI — manage the ScadaLink SCADA system"); -var contactPointsOption = new Option("--contact-points") { Description = "Comma-separated cluster contact points", Recursive = true }; +var urlOption = new Option("--url") { Description = "Management API URL", Recursive = true }; var usernameOption = new Option("--username") { Description = "LDAP username", Recursive = true }; var passwordOption = new Option("--password") { Description = "LDAP password", Recursive = true }; var formatOption = new Option("--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(_ => { diff --git a/src/ScadaLink.CLI/README.md b/src/ScadaLink.CLI/README.md index b92013a..847e00e 100644 --- a/src/ScadaLink.CLI/README.md +++ b/src/ScadaLink.CLI/README.md @@ -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 +scadalink --url http://central-host:5000 ``` -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 +scadalink --url http://localhost:9001 ``` -**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 -``` +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 ` | Comma-separated Akka cluster contact point URIs | +| `--url ` | Management API URL (e.g., `http://localhost:9001`) | | `--username ` | LDAP username for authentication | | `--password ` | LDAP password for authentication | | `--format ` | 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 template list +scadalink --url template list ``` #### `template get` @@ -111,7 +95,7 @@ scadalink --contact-points template list Get a single template by ID. ```sh -scadalink --contact-points template get --id +scadalink --url template get --id ``` | Option | Required | Description | @@ -123,7 +107,7 @@ scadalink --contact-points template get --id Create a new template, optionally inheriting from a parent. ```sh -scadalink --contact-points template create --name [--description ] [--parent-id ] +scadalink --url template create --name [--description ] [--parent-id ] ``` | Option | Required | Description | @@ -137,7 +121,7 @@ scadalink --contact-points template create --name [--description Update an existing template's name, description, or parent. ```sh -scadalink --contact-points template update --id [--name ] [--description ] [--parent-id ] +scadalink --url template update --id [--name ] [--description ] [--parent-id ] ``` | Option | Required | Description | @@ -152,7 +136,7 @@ scadalink --contact-points template update --id [--name ] [- Delete a template by ID. ```sh -scadalink --contact-points template delete --id +scadalink --url template delete --id ``` | Option | Required | Description | @@ -164,7 +148,7 @@ scadalink --contact-points template delete --id Run pre-deployment validation on a template (flattening, naming collisions, script compilation). ```sh -scadalink --contact-points template validate --id +scadalink --url template validate --id ``` | Option | Required | Description | @@ -176,7 +160,7 @@ scadalink --contact-points template validate --id Add an attribute to a template. ```sh -scadalink --contact-points template attribute add --template-id --name --data-type [--default-value ] [--tag-path ] +scadalink --url template attribute add --template-id --name --data-type [--default-value ] [--tag-path ] ``` | Option | Required | Description | @@ -192,7 +176,7 @@ scadalink --contact-points template attribute add --template-id --na Update an attribute on a template. ```sh -scadalink --contact-points template attribute update --template-id --name [--data-type ] [--default-value ] [--tag-path ] +scadalink --url template attribute update --template-id --name [--data-type ] [--default-value ] [--tag-path ] ``` | Option | Required | Description | @@ -208,7 +192,7 @@ scadalink --contact-points template attribute update --template-id - Remove an attribute from a template. ```sh -scadalink --contact-points template attribute delete --template-id --name +scadalink --url template attribute delete --template-id --name ``` | Option | Required | Description | @@ -221,7 +205,7 @@ scadalink --contact-points template attribute delete --template-id - Add an alarm definition to a template. ```sh -scadalink --contact-points template alarm add --template-id --name --trigger-attribute --condition --setpoint [--severity ] [--notification-list ] +scadalink --url template alarm add --template-id --name --trigger-attribute --condition --setpoint [--severity ] [--notification-list ] ``` | Option | Required | Description | @@ -239,7 +223,7 @@ scadalink --contact-points template alarm add --template-id --name < Update an alarm definition on a template. ```sh -scadalink --contact-points template alarm update --template-id --name [--condition ] [--setpoint ] [--severity ] [--notification-list ] +scadalink --url template alarm update --template-id --name [--condition ] [--setpoint ] [--severity ] [--notification-list ] ``` | Option | Required | Description | @@ -256,7 +240,7 @@ scadalink --contact-points template alarm update --template-id --nam Remove an alarm definition from a template. ```sh -scadalink --contact-points template alarm delete --template-id --name +scadalink --url template alarm delete --template-id --name ``` | Option | Required | Description | @@ -269,7 +253,7 @@ scadalink --contact-points template alarm delete --template-id --nam Add a script to a template. ```sh -scadalink --contact-points template script add --template-id --name --trigger-type [--trigger-attribute ] [--interval ] --code +scadalink --url template script add --template-id --name --trigger-type [--trigger-attribute ] [--interval ] --code ``` | Option | Required | Description | @@ -286,7 +270,7 @@ scadalink --contact-points template script add --template-id --name Update a script on a template. ```sh -scadalink --contact-points template script update --template-id --name [--trigger-type ] [--trigger-attribute ] [--interval ] [--code ] +scadalink --url template script update --template-id --name [--trigger-type ] [--trigger-attribute ] [--interval ] [--code ] ``` | Option | Required | Description | @@ -303,7 +287,7 @@ scadalink --contact-points template script update --template-id --na Remove a script from a template. ```sh -scadalink --contact-points template script delete --template-id --name +scadalink --url template script delete --template-id --name ``` | Option | Required | Description | @@ -316,7 +300,7 @@ scadalink --contact-points template script delete --template-id --na Add a feature module composition to a template. ```sh -scadalink --contact-points template composition add --template-id --module-template-id --instance-name +scadalink --url template composition add --template-id --module-template-id --instance-name ``` | Option | Required | Description | @@ -330,7 +314,7 @@ scadalink --contact-points template composition add --template-id -- Remove a feature module composition from a template. ```sh -scadalink --contact-points template composition delete --template-id --instance-name +scadalink --url template composition delete --template-id --instance-name ``` | Option | Required | Description | @@ -347,7 +331,7 @@ scadalink --contact-points template composition delete --template-id Get a single instance by ID. ```sh -scadalink --contact-points instance get --id +scadalink --url instance get --id ``` | Option | Required | Description | @@ -359,7 +343,7 @@ scadalink --contact-points instance get --id List instances, with optional filters. ```sh -scadalink --contact-points instance list [--site-id ] [--template-id ] [--search ] +scadalink --url instance list [--site-id ] [--template-id ] [--search ] ``` | Option | Required | Description | @@ -373,7 +357,7 @@ scadalink --contact-points instance list [--site-id ] [--template-id Create a new instance of a template at a site. ```sh -scadalink --contact-points instance create --name --template-id --site-id [--area-id ] +scadalink --url instance create --name --template-id --site-id [--area-id ] ``` | Option | Required | Description | @@ -388,7 +372,7 @@ scadalink --contact-points instance create --name --template-id < Deploy an instance to its site. Acquires the per-instance operation lock. ```sh -scadalink --contact-points instance deploy --id +scadalink --url instance deploy --id ``` | Option | Required | Description | @@ -400,7 +384,7 @@ scadalink --contact-points instance deploy --id Enable a previously disabled instance. ```sh -scadalink --contact-points instance enable --id +scadalink --url instance enable --id ``` | Option | Required | Description | @@ -412,7 +396,7 @@ scadalink --contact-points instance enable --id Disable a running instance without deleting it. ```sh -scadalink --contact-points instance disable --id +scadalink --url instance disable --id ``` | Option | Required | Description | @@ -424,7 +408,7 @@ scadalink --contact-points instance disable --id Delete an instance. The instance must be disabled first. ```sh -scadalink --contact-points instance delete --id +scadalink --url instance delete --id ``` | Option | Required | Description | @@ -436,7 +420,7 @@ scadalink --contact-points instance delete --id Set data connection bindings for an instance's attributes. ```sh -scadalink --contact-points instance set-bindings --id --bindings +scadalink --url instance set-bindings --id --bindings ``` | Option | Required | Description | @@ -453,7 +437,7 @@ scadalink --contact-points instance set-bindings --id --bindings site get --id +scadalink --url site get --id ``` | Option | Required | Description | @@ -465,7 +449,7 @@ scadalink --contact-points site get --id List all registered sites. ```sh -scadalink --contact-points site list +scadalink --url site list ``` #### `site create` @@ -473,7 +457,7 @@ scadalink --contact-points site list Register a new site. ```sh -scadalink --contact-points site create --name --identifier [--description ] +scadalink --url site create --name --identifier [--description ] ``` | Option | Required | Description | @@ -487,7 +471,7 @@ scadalink --contact-points site create --name --identifier site delete --id +scadalink --url site delete --id ``` | Option | Required | Description | @@ -499,7 +483,7 @@ scadalink --contact-points site delete --id Push compiled artifacts to one or all sites. ```sh -scadalink --contact-points site deploy-artifacts [--site-id ] +scadalink --url site deploy-artifacts [--site-id ] ``` | Option | Required | Description | @@ -511,7 +495,7 @@ scadalink --contact-points site deploy-artifacts [--site-id ] List all areas for a site. ```sh -scadalink --contact-points site area list --site-id +scadalink --url site area list --site-id ``` | Option | Required | Description | @@ -523,7 +507,7 @@ scadalink --contact-points site area list --site-id Create an area within a site. ```sh -scadalink --contact-points site area create --site-id --name [--parent-area-id ] +scadalink --url site area create --site-id --name [--parent-area-id ] ``` | Option | Required | Description | @@ -537,7 +521,7 @@ scadalink --contact-points site area create --site-id --name site area update --id [--name ] [--parent-area-id ] +scadalink --url site area update --id [--name ] [--parent-area-id ] ``` | Option | Required | Description | @@ -551,7 +535,7 @@ scadalink --contact-points site area update --id [--name ] [ Delete an area. Fails if any instances are assigned to it. ```sh -scadalink --contact-points site area delete --id +scadalink --url site area delete --id ``` | Option | Required | Description | @@ -567,7 +551,7 @@ scadalink --contact-points site area delete --id Deploy a single instance (same as `instance deploy`). ```sh -scadalink --contact-points deploy instance --id +scadalink --url deploy instance --id ``` | Option | Required | Description | @@ -579,7 +563,7 @@ scadalink --contact-points deploy instance --id Deploy compiled artifacts to one or all sites (same as `site deploy-artifacts`). ```sh -scadalink --contact-points deploy artifacts [--site-id ] +scadalink --url deploy artifacts [--site-id ] ``` | Option | Required | Description | @@ -591,7 +575,7 @@ scadalink --contact-points deploy artifacts [--site-id ] Query deployment records, with optional filters and pagination. ```sh -scadalink --contact-points deploy status [--instance-id ] [--status ] [--page ] [--page-size ] +scadalink --url deploy status [--instance-id ] [--status ] [--page ] [--page-size ] ``` | Option | Required | Default | Description | @@ -610,7 +594,7 @@ scadalink --contact-points deploy status [--instance-id ] [--status < Get a single data connection by ID. ```sh -scadalink --contact-points data-connection get --id +scadalink --url data-connection get --id ``` | Option | Required | Description | @@ -622,7 +606,7 @@ scadalink --contact-points data-connection get --id List all configured data connections. ```sh -scadalink --contact-points data-connection list +scadalink --url data-connection list ``` #### `data-connection create` @@ -630,7 +614,7 @@ scadalink --contact-points data-connection list Create a new data connection definition. ```sh -scadalink --contact-points data-connection create --name --protocol [--configuration ] +scadalink --url data-connection create --name --protocol [--configuration ] ``` | Option | Required | Description | @@ -644,7 +628,7 @@ scadalink --contact-points data-connection create --name --protoc Update a data connection definition. ```sh -scadalink --contact-points data-connection update --id [--name ] [--protocol ] [--configuration ] +scadalink --url data-connection update --id [--name ] [--protocol ] [--configuration ] ``` | Option | Required | Description | @@ -659,7 +643,7 @@ scadalink --contact-points data-connection update --id [--name data-connection delete --id +scadalink --url data-connection delete --id ``` | Option | Required | Description | @@ -671,7 +655,7 @@ scadalink --contact-points data-connection delete --id Assign a data connection to a site. ```sh -scadalink --contact-points data-connection assign --connection-id --site-id +scadalink --url data-connection assign --connection-id --site-id ``` | Option | Required | Description | @@ -684,7 +668,7 @@ scadalink --contact-points data-connection assign --connection-id -- Remove a data connection assignment from a site. ```sh -scadalink --contact-points data-connection unassign --connection-id --site-id +scadalink --url data-connection unassign --connection-id --site-id ``` | Option | Required | Description | @@ -701,7 +685,7 @@ scadalink --contact-points data-connection unassign --connection-id Get a single external system definition by ID. ```sh -scadalink --contact-points external-system get --id +scadalink --url external-system get --id ``` | Option | Required | Description | @@ -713,7 +697,7 @@ scadalink --contact-points external-system get --id List all external system definitions. ```sh -scadalink --contact-points external-system list +scadalink --url external-system list ``` #### `external-system create` @@ -721,7 +705,7 @@ scadalink --contact-points external-system list Register an external HTTP system that scripts can call. ```sh -scadalink --contact-points external-system create --name --endpoint-url --auth-type [--auth-config ] +scadalink --url external-system create --name --endpoint-url --auth-type [--auth-config ] ``` | Option | Required | Description | @@ -736,7 +720,7 @@ scadalink --contact-points external-system create --name --endpoi Update an external system definition. ```sh -scadalink --contact-points external-system update --id [--name ] [--endpoint-url ] [--auth-type ] [--auth-config ] +scadalink --url external-system update --id [--name ] [--endpoint-url ] [--auth-type ] [--auth-config ] ``` | Option | Required | Description | @@ -752,7 +736,7 @@ scadalink --contact-points external-system update --id [--name external-system delete --id +scadalink --url external-system delete --id ``` | Option | Required | Description | @@ -768,7 +752,7 @@ scadalink --contact-points external-system delete --id Get a single notification list by ID. ```sh -scadalink --contact-points notification get --id +scadalink --url notification get --id ``` | Option | Required | Description | @@ -780,7 +764,7 @@ scadalink --contact-points notification get --id List all notification lists. ```sh -scadalink --contact-points notification list +scadalink --url notification list ``` #### `notification create` @@ -788,7 +772,7 @@ scadalink --contact-points notification list Create a notification list with one or more recipients. ```sh -scadalink --contact-points notification create --name --emails +scadalink --url notification create --name --emails ``` | Option | Required | Description | @@ -801,7 +785,7 @@ scadalink --contact-points notification create --name --emails notification update --id [--name ] [--emails ] +scadalink --url notification update --id [--name ] [--emails ] ``` | Option | Required | Description | @@ -815,7 +799,7 @@ scadalink --contact-points notification update --id [--name Delete a notification list. ```sh -scadalink --contact-points notification delete --id +scadalink --url notification delete --id ``` | Option | Required | Description | @@ -827,7 +811,7 @@ scadalink --contact-points notification delete --id Show the current SMTP configuration. ```sh -scadalink --contact-points notification smtp list +scadalink --url notification smtp list ``` #### `notification smtp update` @@ -835,7 +819,7 @@ scadalink --contact-points notification smtp list Update the SMTP configuration. ```sh -scadalink --contact-points notification smtp update --host --port --auth-type [--username ] [--password ] [--from-address ] +scadalink --url notification smtp update --host --port --auth-type [--username ] [--password ] [--from-address ] ``` | Option | Required | Description | @@ -856,7 +840,7 @@ scadalink --contact-points notification smtp update --host --port List all inbound API keys. ```sh -scadalink --contact-points security api-key list +scadalink --url security api-key list ``` #### `security api-key create` @@ -864,7 +848,7 @@ scadalink --contact-points 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 security api-key create --name +scadalink --url security api-key create --name ``` | Option | Required | Description | @@ -876,7 +860,7 @@ scadalink --contact-points security api-key create --name Update an API key's name or enabled status. ```sh -scadalink --contact-points security api-key update --id [--name ] [--enabled ] +scadalink --url security api-key update --id [--name ] [--enabled ] ``` | Option | Required | Description | @@ -890,7 +874,7 @@ scadalink --contact-points security api-key update --id [--name security api-key delete --id +scadalink --url security api-key delete --id ``` | Option | Required | Description | @@ -902,7 +886,7 @@ scadalink --contact-points security api-key delete --id List all LDAP group → role mappings. ```sh -scadalink --contact-points security role-mapping list +scadalink --url security role-mapping list ``` #### `security role-mapping create` @@ -910,7 +894,7 @@ scadalink --contact-points security role-mapping list Map an LDAP group to a ScadaLink role. ```sh -scadalink --contact-points security role-mapping create --ldap-group --role +scadalink --url security role-mapping create --ldap-group --role ``` | Option | Required | Description | @@ -923,7 +907,7 @@ scadalink --contact-points security role-mapping create --ldap-group security role-mapping update --id [--ldap-group ] [--role ] +scadalink --url security role-mapping update --id [--ldap-group ] [--role ] ``` | Option | Required | Description | @@ -937,7 +921,7 @@ scadalink --contact-points security role-mapping update --id [--ldap Remove an LDAP role mapping. ```sh -scadalink --contact-points security role-mapping delete --id +scadalink --url security role-mapping delete --id ``` | Option | Required | Description | @@ -949,7 +933,7 @@ scadalink --contact-points security role-mapping delete --id List all site scope rules for role mappings. ```sh -scadalink --contact-points security scope-rule list [--role-mapping-id ] +scadalink --url security scope-rule list [--role-mapping-id ] ``` | Option | Required | Description | @@ -961,7 +945,7 @@ scadalink --contact-points security scope-rule list [--role-mapping-id security scope-rule add --role-mapping-id --site-id +scadalink --url security scope-rule add --role-mapping-id --site-id ``` | Option | Required | Description | @@ -974,7 +958,7 @@ scadalink --contact-points security scope-rule add --role-mapping-id Remove a site scope rule from a role mapping. ```sh -scadalink --contact-points security scope-rule delete --id +scadalink --url security scope-rule delete --id ``` | Option | Required | Description | @@ -990,7 +974,7 @@ scadalink --contact-points security scope-rule delete --id Return the current health state for all known sites as a JSON object keyed by site identifier. ```sh -scadalink --contact-points health summary +scadalink --url health summary ``` #### `health site` @@ -998,7 +982,7 @@ scadalink --contact-points health summary Return the health state for a single site. ```sh -scadalink --contact-points health site --identifier +scadalink --url health site --identifier ``` | Option | Required | Description | @@ -1010,7 +994,7 @@ scadalink --contact-points health site --identifier Query the site event log for a specific site. Events are fetched remotely from the site's local SQLite store. ```sh -scadalink --contact-points health event-log --site-identifier [--from ] [--to ] [--search ] [--page ] [--page-size ] +scadalink --url health event-log --site-identifier [--from ] [--to ] [--search ] [--page ] [--page-size ] ``` | Option | Required | Default | Description | @@ -1027,7 +1011,7 @@ scadalink --contact-points health event-log --site-identifier [-- Query parked (dead-letter) messages at a specific site. ```sh -scadalink --contact-points health parked-messages --site-identifier [--page ] [--page-size ] +scadalink --url health parked-messages --site-identifier [--page ] [--page-size ] ``` | Option | Required | Default | Description | @@ -1045,7 +1029,7 @@ scadalink --contact-points health parked-messages --site-identifier debug snapshot --id +scadalink --url debug snapshot --id ``` | 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 audit-log query [options] +scadalink --url audit-log query [options] ``` | Option | Required | Default | Description | @@ -1085,7 +1069,7 @@ scadalink --contact-points audit-log query [options] List all shared script definitions. ```sh -scadalink --contact-points shared-script list +scadalink --url shared-script list ``` #### `shared-script get` @@ -1093,7 +1077,7 @@ scadalink --contact-points shared-script list Get a single shared script by ID. ```sh -scadalink --contact-points shared-script get --id +scadalink --url shared-script get --id ``` | Option | Required | Description | @@ -1105,7 +1089,7 @@ scadalink --contact-points shared-script get --id Create a new shared script. ```sh -scadalink --contact-points shared-script create --name --code +scadalink --url shared-script create --name --code ``` | Option | Required | Description | @@ -1118,7 +1102,7 @@ scadalink --contact-points shared-script create --name --code shared-script update --id [--name ] [--code ] +scadalink --url shared-script update --id [--name ] [--code ] ``` | Option | Required | Description | @@ -1132,7 +1116,7 @@ scadalink --contact-points shared-script update --id [--name shared-script delete --id +scadalink --url shared-script delete --id ``` | Option | Required | Description | @@ -1148,7 +1132,7 @@ scadalink --contact-points shared-script delete --id List all database connection definitions. ```sh -scadalink --contact-points db-connection list +scadalink --url db-connection list ``` #### `db-connection get` @@ -1156,7 +1140,7 @@ scadalink --contact-points db-connection list Get a single database connection by ID. ```sh -scadalink --contact-points db-connection get --id +scadalink --url db-connection get --id ``` | Option | Required | Description | @@ -1168,7 +1152,7 @@ scadalink --contact-points db-connection get --id Create a new database connection definition. ```sh -scadalink --contact-points db-connection create --name --connection-string [--provider ] +scadalink --url db-connection create --name --connection-string [--provider ] ``` | Option | Required | Description | @@ -1182,7 +1166,7 @@ scadalink --contact-points db-connection create --name --connecti Update a database connection definition. ```sh -scadalink --contact-points db-connection update --id [--name ] [--connection-string ] [--provider ] +scadalink --url db-connection update --id [--name ] [--connection-string ] [--provider ] ``` | Option | Required | Description | @@ -1197,7 +1181,7 @@ scadalink --contact-points db-connection update --id [--name db-connection delete --id +scadalink --url db-connection delete --id ``` | Option | Required | Description | @@ -1213,7 +1197,7 @@ scadalink --contact-points db-connection delete --id List all inbound API method definitions. ```sh -scadalink --contact-points api-method list +scadalink --url api-method list ``` #### `api-method get` @@ -1221,7 +1205,7 @@ scadalink --contact-points api-method list Get a single inbound API method by ID. ```sh -scadalink --contact-points api-method get --id +scadalink --url api-method get --id ``` | Option | Required | Description | @@ -1233,7 +1217,7 @@ scadalink --contact-points api-method get --id Create a new inbound API method. ```sh -scadalink --contact-points api-method create --name --code [--description ] +scadalink --url api-method create --name --code [--description ] ``` | Option | Required | Description | @@ -1247,7 +1231,7 @@ scadalink --contact-points api-method create --name --code api-method update --id [--name ] [--code ] [--description ] +scadalink --url api-method update --id [--name ] [--code ] [--description ] ``` | Option | Required | Description | @@ -1262,7 +1246,7 @@ scadalink --contact-points api-method update --id [--name ] Delete an inbound API method. ```sh -scadalink --contact-points api-method delete --id +scadalink --url api-method delete --id ``` | Option | Required | Description | diff --git a/src/ScadaLink.CLI/ScadaLink.CLI.csproj b/src/ScadaLink.CLI/ScadaLink.CLI.csproj index 4125f6d..837e5ae 100644 --- a/src/ScadaLink.CLI/ScadaLink.CLI.csproj +++ b/src/ScadaLink.CLI/ScadaLink.CLI.csproj @@ -11,16 +11,9 @@ - - - - - - - diff --git a/src/ScadaLink.Commons/Messages/Management/ManagementCommandRegistry.cs b/src/ScadaLink.Commons/Messages/Management/ManagementCommandRegistry.cs new file mode 100644 index 0000000..002eef5 --- /dev/null +++ b/src/ScadaLink.Commons/Messages/Management/ManagementCommandRegistry.cs @@ -0,0 +1,36 @@ +using System.Collections.Frozen; + +namespace ScadaLink.Commons.Messages.Management; + +public static class ManagementCommandRegistry +{ + private static readonly FrozenDictionary 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 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); + } +} diff --git a/src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs b/src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs index 420ec49..37fa73a 100644 --- a/src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs +++ b/src/ScadaLink.DataConnectionLayer/Actors/DataConnectionActor.cs @@ -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) diff --git a/src/ScadaLink.Host/Actors/AkkaHostedService.cs b/src/ScadaLink.Host/Actors/AkkaHostedService.cs index b8b8a8c..a8b421b 100644 --- a/src/ScadaLink.Host/Actors/AkkaHostedService.cs +++ b/src/ScadaLink.Host/Actors/AkkaHostedService.cs @@ -192,6 +192,8 @@ akka {{ Props.Create(() => new ScadaLink.ManagementService.ManagementActor(_serviceProvider, mgmtLogger)), "management"); ClusterClientReceptionist.Get(_actorSystem).RegisterService(mgmtActor); + var mgmtHolder = _serviceProvider.GetRequiredService(); + mgmtHolder.ActorRef = mgmtActor; _logger.LogInformation("ManagementActor registered with ClusterClientReceptionist"); _logger.LogInformation("Central actors registered. CentralCommunicationActor created."); diff --git a/src/ScadaLink.Host/Program.cs b/src/ScadaLink.Host/Program.cs index cdedc71..7deff27 100644 --- a/src/ScadaLink.Host/Program.cs +++ b/src/ScadaLink.Host/Program.cs @@ -129,6 +129,7 @@ try app.MapStaticAssets(); app.MapCentralUI(); app.MapInboundAPI(); + app.MapManagementAPI(); // Compile and register all Inbound API method scripts at startup using (var scope = app.Services.CreateScope()) diff --git a/src/ScadaLink.ManagementService/ManagementActorHolder.cs b/src/ScadaLink.ManagementService/ManagementActorHolder.cs new file mode 100644 index 0000000..57598ae --- /dev/null +++ b/src/ScadaLink.ManagementService/ManagementActorHolder.cs @@ -0,0 +1,8 @@ +using Akka.Actor; + +namespace ScadaLink.ManagementService; + +public class ManagementActorHolder +{ + public IActorRef? ActorRef { get; set; } +} diff --git a/src/ScadaLink.ManagementService/ManagementEndpoints.cs b/src/ScadaLink.ManagementService/ManagementEndpoints.cs new file mode 100644 index 0000000..4f1c83e --- /dev/null +++ b/src/ScadaLink.ManagementService/ManagementEndpoints.cs @@ -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 HandleRequest(HttpContext context) + { + var logger = context.RequestServices.GetRequiredService>(); + + // 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(); + 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(); + var mappingResult = await roleMapper.MapGroupsToRolesAsync( + authResult.Groups ?? (IReadOnlyList)Array.Empty()); + + var permittedSiteIds = mappingResult.IsSystemWideDeployment + ? Array.Empty() + : 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(); + 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) + }; + } +} diff --git a/src/ScadaLink.ManagementService/ScadaLink.ManagementService.csproj b/src/ScadaLink.ManagementService/ScadaLink.ManagementService.csproj index 64d6ba6..bc713b5 100644 --- a/src/ScadaLink.ManagementService/ScadaLink.ManagementService.csproj +++ b/src/ScadaLink.ManagementService/ScadaLink.ManagementService.csproj @@ -5,12 +5,12 @@ enable true + + + - - - diff --git a/src/ScadaLink.ManagementService/ServiceCollectionExtensions.cs b/src/ScadaLink.ManagementService/ServiceCollectionExtensions.cs index 0ca9dc5..6a3e260 100644 --- a/src/ScadaLink.ManagementService/ServiceCollectionExtensions.cs +++ b/src/ScadaLink.ManagementService/ServiceCollectionExtensions.cs @@ -6,6 +6,7 @@ public static class ServiceCollectionExtensions { public static IServiceCollection AddManagementService(this IServiceCollection services) { + services.AddSingleton(); services.AddOptions() .BindConfiguration("ScadaLink:ManagementService"); return services; diff --git a/tests/ScadaLink.CLI.Tests/CliConfigTests.cs b/tests/ScadaLink.CLI.Tests/CliConfigTests.cs index 9a37ab2..0c179e4 100644 --- a/tests/ScadaLink.CLI.Tests/CliConfigTests.cs +++ b/tests/ScadaLink.CLI.Tests/CliConfigTests.cs @@ -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); } } diff --git a/tests/ScadaLink.CLI.Tests/CommandHelpersTests.cs b/tests/ScadaLink.CLI.Tests/CommandHelpersTests.cs index b74b37b..51d84da 100644 --- a/tests/ScadaLink.CLI.Tests/CommandHelpersTests.cs +++ b/tests/ScadaLink.CLI.Tests/CommandHelpersTests.cs @@ -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 }); } } diff --git a/tests/ScadaLink.Commons.Tests/ArchitecturalConstraintTests.cs b/tests/ScadaLink.Commons.Tests/ArchitecturalConstraintTests.cs index 356ff54..e3346d1 100644 --- a/tests/ScadaLink.Commons.Tests/ArchitecturalConstraintTests.cs +++ b/tests/ScadaLink.Commons.Tests/ArchitecturalConstraintTests.cs @@ -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) diff --git a/tests/ScadaLink.Commons.Tests/Messages/MessageConventionTests.cs b/tests/ScadaLink.Commons.Tests/Messages/MessageConventionTests.cs index ab108e7..ce43828 100644 --- a/tests/ScadaLink.Commons.Tests/Messages/MessageConventionTests.cs +++ b/tests/ScadaLink.Commons.Tests/Messages/MessageConventionTests.cs @@ -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]