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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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