feat(cli): add --primary-config, --backup-config, --failover-retry-count to data connection commands

Thread backup data connection fields through management command messages,
ManagementActor handlers, SiteService, site-side SQLite storage, and
deployment/replication actors. The old --configuration CLI flag is kept
as a hidden alias for backwards compatibility.
This commit is contained in:
Joseph Doherty
2026-03-22 08:41:57 -04:00
parent ab4e88f17f
commit e8df71ea64
8 changed files with 63 additions and 23 deletions

View File

@@ -38,22 +38,28 @@ public static class DataConnectionCommands
var idOption = new Option<int>("--id") { Description = "Data connection ID", Required = true };
var nameOption = new Option<string>("--name") { Description = "Connection name", Required = true };
var protocolOption = new Option<string>("--protocol") { Description = "Protocol", Required = true };
var configOption = new Option<string?>("--configuration") { Description = "Primary configuration JSON" };
var configOption = new Option<string?>("--primary-config", "--configuration") { Description = "Primary configuration JSON" };
var backupConfigOption = new Option<string?>("--backup-config") { Description = "Backup configuration JSON" };
var failoverRetryOption = new Option<int>("--failover-retry-count") { Description = "Number of retries before failover to backup", DefaultValueFactory = _ => 3 };
var cmd = new Command("update") { Description = "Update a data connection" };
cmd.Add(idOption);
cmd.Add(nameOption);
cmd.Add(protocolOption);
cmd.Add(configOption);
cmd.Add(backupConfigOption);
cmd.Add(failoverRetryOption);
cmd.SetAction(async (ParseResult result) =>
{
var id = result.GetValue(idOption);
var name = result.GetValue(nameOption)!;
var protocol = result.GetValue(protocolOption)!;
var config = result.GetValue(configOption);
var backupConfig = result.GetValue(backupConfigOption);
var failoverRetryCount = result.GetValue(failoverRetryOption);
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new UpdateDataConnectionCommand(id, name, protocol, config));
new UpdateDataConnectionCommand(id, name, protocol, config, backupConfig, failoverRetryCount));
});
return cmd;
}
@@ -77,22 +83,28 @@ public static class DataConnectionCommands
var siteIdOption = new Option<int>("--site-id") { Description = "Site ID", Required = true };
var nameOption = new Option<string>("--name") { Description = "Connection name", Required = true };
var protocolOption = new Option<string>("--protocol") { Description = "Protocol (e.g. OpcUa)", Required = true };
var configOption = new Option<string?>("--configuration") { Description = "Primary configuration JSON" };
var configOption = new Option<string?>("--primary-config", "--configuration") { Description = "Primary configuration JSON" };
var backupConfigOption = new Option<string?>("--backup-config") { Description = "Backup configuration JSON" };
var failoverRetryOption = new Option<int>("--failover-retry-count") { Description = "Number of retries before failover to backup", DefaultValueFactory = _ => 3 };
var cmd = new Command("create") { Description = "Create a new data connection" };
cmd.Add(siteIdOption);
cmd.Add(nameOption);
cmd.Add(protocolOption);
cmd.Add(configOption);
cmd.Add(backupConfigOption);
cmd.Add(failoverRetryOption);
cmd.SetAction(async (ParseResult result) =>
{
var siteId = result.GetValue(siteIdOption);
var name = result.GetValue(nameOption)!;
var protocol = result.GetValue(protocolOption)!;
var config = result.GetValue(configOption);
var backupConfig = result.GetValue(backupConfigOption);
var failoverRetryCount = result.GetValue(failoverRetryOption);
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
new CreateDataConnectionCommand(siteId, name, protocol, config));
new CreateDataConnectionCommand(siteId, name, protocol, config, backupConfig, failoverRetryCount));
});
return cmd;
}

View File

@@ -2,6 +2,6 @@ namespace ScadaLink.Commons.Messages.Management;
public record ListDataConnectionsCommand(int? SiteId = null);
public record GetDataConnectionCommand(int DataConnectionId);
public record CreateDataConnectionCommand(int SiteId, string Name, string Protocol, string? PrimaryConfiguration);
public record UpdateDataConnectionCommand(int DataConnectionId, string Name, string Protocol, string? PrimaryConfiguration);
public record CreateDataConnectionCommand(int SiteId, string Name, string Protocol, string? PrimaryConfiguration, string? BackupConfiguration = null, int FailoverRetryCount = 3);
public record UpdateDataConnectionCommand(int DataConnectionId, string Name, string Protocol, string? PrimaryConfiguration, string? BackupConfiguration = null, int FailoverRetryCount = 3);
public record DeleteDataConnectionCommand(int DataConnectionId);

View File

@@ -689,7 +689,12 @@ public class ManagementActor : ReceiveActor
private static async Task<object?> HandleCreateDataConnection(IServiceProvider sp, CreateDataConnectionCommand cmd, string user)
{
var repo = sp.GetRequiredService<ISiteRepository>();
var conn = new DataConnection(cmd.Name, cmd.Protocol, cmd.SiteId) { PrimaryConfiguration = cmd.PrimaryConfiguration };
var conn = new DataConnection(cmd.Name, cmd.Protocol, cmd.SiteId)
{
PrimaryConfiguration = cmd.PrimaryConfiguration,
BackupConfiguration = cmd.BackupConfiguration,
FailoverRetryCount = cmd.FailoverRetryCount
};
await repo.AddDataConnectionAsync(conn);
await repo.SaveChangesAsync();
await AuditAsync(sp, user, "Create", "DataConnection", conn.Id.ToString(), conn.Name, conn);
@@ -704,6 +709,8 @@ public class ManagementActor : ReceiveActor
conn.Name = cmd.Name;
conn.Protocol = cmd.Protocol;
conn.PrimaryConfiguration = cmd.PrimaryConfiguration;
conn.BackupConfiguration = cmd.BackupConfiguration;
conn.FailoverRetryCount = cmd.FailoverRetryCount;
await repo.UpdateDataConnectionAsync(conn);
await repo.SaveChangesAsync();
await AuditAsync(sp, user, "Update", "DataConnection", conn.Id.ToString(), conn.Name, conn);

View File

@@ -630,7 +630,8 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
foreach (var dc in command.DataConnections)
{
await _storage.StoreDataConnectionDefinitionAsync(
dc.Name, dc.Protocol, dc.PrimaryConfigurationJson);
dc.Name, dc.Protocol, dc.PrimaryConfigurationJson,
dc.BackupConfigurationJson, dc.FailoverRetryCount);
}
}

View File

@@ -185,7 +185,7 @@ public class SiteReplicationActor : ReceiveActor
if (command.DataConnections != null)
foreach (var dc in command.DataConnections)
await _storage.StoreDataConnectionDefinitionAsync(dc.Name, dc.Protocol, dc.PrimaryConfigurationJson);
await _storage.StoreDataConnectionDefinitionAsync(dc.Name, dc.Protocol, dc.PrimaryConfigurationJson, dc.BackupConfigurationJson, dc.FailoverRetryCount);
if (command.SmtpConfigurations != null)
foreach (var smtp in command.SmtpConfigurations)

View File

@@ -82,6 +82,8 @@ public class SiteStorageService
name TEXT PRIMARY KEY,
protocol TEXT NOT NULL,
configuration TEXT,
backup_configuration TEXT,
failover_retry_count INTEGER NOT NULL DEFAULT 3,
updated_at TEXT NOT NULL
);
@@ -480,23 +482,28 @@ public class SiteStorageService
/// <summary>
/// Stores or updates a data connection definition (OPC UA endpoint, etc.).
/// </summary>
public async Task StoreDataConnectionDefinitionAsync(string name, string protocol, string? configJson)
public async Task StoreDataConnectionDefinitionAsync(
string name, string protocol, string? configJson, string? backupConfigJson = null, int failoverRetryCount = 3)
{
await using var connection = new SqliteConnection(_connectionString);
await connection.OpenAsync();
await using var command = connection.CreateCommand();
command.CommandText = @"
INSERT INTO data_connection_definitions (name, protocol, configuration, updated_at)
VALUES (@name, @protocol, @config, @updatedAt)
INSERT INTO data_connection_definitions (name, protocol, configuration, backup_configuration, failover_retry_count, updated_at)
VALUES (@name, @protocol, @config, @backupConfig, @failoverRetryCount, @updatedAt)
ON CONFLICT(name) DO UPDATE SET
protocol = excluded.protocol,
configuration = excluded.configuration,
backup_configuration = excluded.backup_configuration,
failover_retry_count = excluded.failover_retry_count,
updated_at = excluded.updated_at";
command.Parameters.AddWithValue("@name", name);
command.Parameters.AddWithValue("@protocol", protocol);
command.Parameters.AddWithValue("@config", (object?)configJson ?? DBNull.Value);
command.Parameters.AddWithValue("@backupConfig", (object?)backupConfigJson ?? DBNull.Value);
command.Parameters.AddWithValue("@failoverRetryCount", failoverRetryCount);
command.Parameters.AddWithValue("@updatedAt", DateTimeOffset.UtcNow.ToString("O"));
await command.ExecuteNonQueryAsync();
@@ -512,7 +519,7 @@ public class SiteStorageService
await connection.OpenAsync();
await using var command = connection.CreateCommand();
command.CommandText = "SELECT name, protocol, configuration FROM data_connection_definitions";
command.CommandText = "SELECT name, protocol, configuration, backup_configuration, failover_retry_count FROM data_connection_definitions";
var results = new List<StoredDataConnectionDefinition>();
await using var reader = await command.ExecuteReaderAsync();
@@ -522,7 +529,9 @@ public class SiteStorageService
{
Name = reader.GetString(0),
Protocol = reader.GetString(1),
ConfigurationJson = reader.IsDBNull(2) ? null : reader.GetString(2)
ConfigurationJson = reader.IsDBNull(2) ? null : reader.GetString(2),
BackupConfigurationJson = reader.IsDBNull(3) ? null : reader.GetString(3),
FailoverRetryCount = reader.GetInt32(4)
});
}
@@ -562,4 +571,6 @@ public class StoredDataConnectionDefinition
public string Name { get; init; } = string.Empty;
public string Protocol { get; init; } = string.Empty;
public string? ConfigurationJson { get; init; }
public string? BackupConfigurationJson { get; init; }
public int FailoverRetryCount { get; init; } = 3;
}

View File

@@ -96,7 +96,8 @@ public class SiteService
// --- Data Connection CRUD ---
public async Task<Result<DataConnection>> CreateDataConnectionAsync(
int siteId, string name, string protocol, string? configuration, string user,
int siteId, string name, string protocol, string? primaryConfiguration,
string? backupConfiguration, int failoverRetryCount, string user,
CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(name))
@@ -104,7 +105,12 @@ public class SiteService
if (string.IsNullOrWhiteSpace(protocol))
return Result<DataConnection>.Failure("Protocol is required.");
var connection = new DataConnection(name, protocol, siteId) { PrimaryConfiguration = configuration };
var connection = new DataConnection(name, protocol, siteId)
{
PrimaryConfiguration = primaryConfiguration,
BackupConfiguration = backupConfiguration,
FailoverRetryCount = failoverRetryCount
};
await _repository.AddDataConnectionAsync(connection, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
@@ -115,7 +121,8 @@ public class SiteService
}
public async Task<Result<DataConnection>> UpdateDataConnectionAsync(
int connectionId, string name, string protocol, string? configuration, string user,
int connectionId, string name, string protocol, string? primaryConfiguration,
string? backupConfiguration, int failoverRetryCount, string user,
CancellationToken cancellationToken = default)
{
var connection = await _repository.GetDataConnectionByIdAsync(connectionId, cancellationToken);
@@ -124,7 +131,9 @@ public class SiteService
connection.Name = name;
connection.Protocol = protocol;
connection.PrimaryConfiguration = configuration;
connection.PrimaryConfiguration = primaryConfiguration;
connection.BackupConfiguration = backupConfiguration;
connection.FailoverRetryCount = failoverRetryCount;
await _repository.UpdateDataConnectionAsync(connection, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);

View File

@@ -93,7 +93,7 @@ public class SiteServiceTests
_repoMock.Setup(r => r.SaveChangesAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(1);
var result = await _sut.CreateDataConnectionAsync(1, "OPC-Server1", "OpcUa", "{\"url\":\"opc.tcp://localhost\"}", "admin");
var result = await _sut.CreateDataConnectionAsync(1, "OPC-Server1", "OpcUa", "{\"url\":\"opc.tcp://localhost\"}", null, 3, "admin");
Assert.True(result.IsSuccess);
Assert.Equal("OPC-Server1", result.Value.Name);