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:
@@ -38,22 +38,28 @@ public static class DataConnectionCommands
|
|||||||
var idOption = new Option<int>("--id") { Description = "Data connection ID", Required = true };
|
var idOption = new Option<int>("--id") { Description = "Data connection ID", Required = true };
|
||||||
var nameOption = new Option<string>("--name") { Description = "Connection name", Required = true };
|
var nameOption = new Option<string>("--name") { Description = "Connection name", Required = true };
|
||||||
var protocolOption = new Option<string>("--protocol") { Description = "Protocol", 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" };
|
var cmd = new Command("update") { Description = "Update a data connection" };
|
||||||
cmd.Add(idOption);
|
cmd.Add(idOption);
|
||||||
cmd.Add(nameOption);
|
cmd.Add(nameOption);
|
||||||
cmd.Add(protocolOption);
|
cmd.Add(protocolOption);
|
||||||
cmd.Add(configOption);
|
cmd.Add(configOption);
|
||||||
|
cmd.Add(backupConfigOption);
|
||||||
|
cmd.Add(failoverRetryOption);
|
||||||
cmd.SetAction(async (ParseResult result) =>
|
cmd.SetAction(async (ParseResult result) =>
|
||||||
{
|
{
|
||||||
var id = result.GetValue(idOption);
|
var id = result.GetValue(idOption);
|
||||||
var name = result.GetValue(nameOption)!;
|
var name = result.GetValue(nameOption)!;
|
||||||
var protocol = result.GetValue(protocolOption)!;
|
var protocol = result.GetValue(protocolOption)!;
|
||||||
var config = result.GetValue(configOption);
|
var config = result.GetValue(configOption);
|
||||||
|
var backupConfig = result.GetValue(backupConfigOption);
|
||||||
|
var failoverRetryCount = result.GetValue(failoverRetryOption);
|
||||||
return await CommandHelpers.ExecuteCommandAsync(
|
return await CommandHelpers.ExecuteCommandAsync(
|
||||||
result, urlOption, formatOption, usernameOption, passwordOption,
|
result, urlOption, formatOption, usernameOption, passwordOption,
|
||||||
new UpdateDataConnectionCommand(id, name, protocol, config));
|
new UpdateDataConnectionCommand(id, name, protocol, config, backupConfig, failoverRetryCount));
|
||||||
});
|
});
|
||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
@@ -77,22 +83,28 @@ public static class DataConnectionCommands
|
|||||||
var siteIdOption = new Option<int>("--site-id") { Description = "Site ID", Required = true };
|
var siteIdOption = new Option<int>("--site-id") { Description = "Site ID", Required = true };
|
||||||
var nameOption = new Option<string>("--name") { Description = "Connection name", 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 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" };
|
var cmd = new Command("create") { Description = "Create a new data connection" };
|
||||||
cmd.Add(siteIdOption);
|
cmd.Add(siteIdOption);
|
||||||
cmd.Add(nameOption);
|
cmd.Add(nameOption);
|
||||||
cmd.Add(protocolOption);
|
cmd.Add(protocolOption);
|
||||||
cmd.Add(configOption);
|
cmd.Add(configOption);
|
||||||
|
cmd.Add(backupConfigOption);
|
||||||
|
cmd.Add(failoverRetryOption);
|
||||||
cmd.SetAction(async (ParseResult result) =>
|
cmd.SetAction(async (ParseResult result) =>
|
||||||
{
|
{
|
||||||
var siteId = result.GetValue(siteIdOption);
|
var siteId = result.GetValue(siteIdOption);
|
||||||
var name = result.GetValue(nameOption)!;
|
var name = result.GetValue(nameOption)!;
|
||||||
var protocol = result.GetValue(protocolOption)!;
|
var protocol = result.GetValue(protocolOption)!;
|
||||||
var config = result.GetValue(configOption);
|
var config = result.GetValue(configOption);
|
||||||
|
var backupConfig = result.GetValue(backupConfigOption);
|
||||||
|
var failoverRetryCount = result.GetValue(failoverRetryOption);
|
||||||
return await CommandHelpers.ExecuteCommandAsync(
|
return await CommandHelpers.ExecuteCommandAsync(
|
||||||
result, urlOption, formatOption, usernameOption, passwordOption,
|
result, urlOption, formatOption, usernameOption, passwordOption,
|
||||||
new CreateDataConnectionCommand(siteId, name, protocol, config));
|
new CreateDataConnectionCommand(siteId, name, protocol, config, backupConfig, failoverRetryCount));
|
||||||
});
|
});
|
||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,6 @@ namespace ScadaLink.Commons.Messages.Management;
|
|||||||
|
|
||||||
public record ListDataConnectionsCommand(int? SiteId = null);
|
public record ListDataConnectionsCommand(int? SiteId = null);
|
||||||
public record GetDataConnectionCommand(int DataConnectionId);
|
public record GetDataConnectionCommand(int DataConnectionId);
|
||||||
public record CreateDataConnectionCommand(int SiteId, 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);
|
public record UpdateDataConnectionCommand(int DataConnectionId, string Name, string Protocol, string? PrimaryConfiguration, string? BackupConfiguration = null, int FailoverRetryCount = 3);
|
||||||
public record DeleteDataConnectionCommand(int DataConnectionId);
|
public record DeleteDataConnectionCommand(int DataConnectionId);
|
||||||
|
|||||||
@@ -689,7 +689,12 @@ public class ManagementActor : ReceiveActor
|
|||||||
private static async Task<object?> HandleCreateDataConnection(IServiceProvider sp, CreateDataConnectionCommand cmd, string user)
|
private static async Task<object?> HandleCreateDataConnection(IServiceProvider sp, CreateDataConnectionCommand cmd, string user)
|
||||||
{
|
{
|
||||||
var repo = sp.GetRequiredService<ISiteRepository>();
|
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.AddDataConnectionAsync(conn);
|
||||||
await repo.SaveChangesAsync();
|
await repo.SaveChangesAsync();
|
||||||
await AuditAsync(sp, user, "Create", "DataConnection", conn.Id.ToString(), conn.Name, conn);
|
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.Name = cmd.Name;
|
||||||
conn.Protocol = cmd.Protocol;
|
conn.Protocol = cmd.Protocol;
|
||||||
conn.PrimaryConfiguration = cmd.PrimaryConfiguration;
|
conn.PrimaryConfiguration = cmd.PrimaryConfiguration;
|
||||||
|
conn.BackupConfiguration = cmd.BackupConfiguration;
|
||||||
|
conn.FailoverRetryCount = cmd.FailoverRetryCount;
|
||||||
await repo.UpdateDataConnectionAsync(conn);
|
await repo.UpdateDataConnectionAsync(conn);
|
||||||
await repo.SaveChangesAsync();
|
await repo.SaveChangesAsync();
|
||||||
await AuditAsync(sp, user, "Update", "DataConnection", conn.Id.ToString(), conn.Name, conn);
|
await AuditAsync(sp, user, "Update", "DataConnection", conn.Id.ToString(), conn.Name, conn);
|
||||||
|
|||||||
@@ -630,7 +630,8 @@ public class DeploymentManagerActor : ReceiveActor, IWithTimers
|
|||||||
foreach (var dc in command.DataConnections)
|
foreach (var dc in command.DataConnections)
|
||||||
{
|
{
|
||||||
await _storage.StoreDataConnectionDefinitionAsync(
|
await _storage.StoreDataConnectionDefinitionAsync(
|
||||||
dc.Name, dc.Protocol, dc.PrimaryConfigurationJson);
|
dc.Name, dc.Protocol, dc.PrimaryConfigurationJson,
|
||||||
|
dc.BackupConfigurationJson, dc.FailoverRetryCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ public class SiteReplicationActor : ReceiveActor
|
|||||||
|
|
||||||
if (command.DataConnections != null)
|
if (command.DataConnections != null)
|
||||||
foreach (var dc in command.DataConnections)
|
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)
|
if (command.SmtpConfigurations != null)
|
||||||
foreach (var smtp in command.SmtpConfigurations)
|
foreach (var smtp in command.SmtpConfigurations)
|
||||||
|
|||||||
@@ -79,10 +79,12 @@ public class SiteStorageService
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS data_connection_definitions (
|
CREATE TABLE IF NOT EXISTS data_connection_definitions (
|
||||||
name TEXT PRIMARY KEY,
|
name TEXT PRIMARY KEY,
|
||||||
protocol TEXT NOT NULL,
|
protocol TEXT NOT NULL,
|
||||||
configuration TEXT,
|
configuration TEXT,
|
||||||
updated_at TEXT NOT NULL
|
backup_configuration TEXT,
|
||||||
|
failover_retry_count INTEGER NOT NULL DEFAULT 3,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS smtp_configurations (
|
CREATE TABLE IF NOT EXISTS smtp_configurations (
|
||||||
@@ -480,23 +482,28 @@ public class SiteStorageService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stores or updates a data connection definition (OPC UA endpoint, etc.).
|
/// Stores or updates a data connection definition (OPC UA endpoint, etc.).
|
||||||
/// </summary>
|
/// </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 using var connection = new SqliteConnection(_connectionString);
|
||||||
await connection.OpenAsync();
|
await connection.OpenAsync();
|
||||||
|
|
||||||
await using var command = connection.CreateCommand();
|
await using var command = connection.CreateCommand();
|
||||||
command.CommandText = @"
|
command.CommandText = @"
|
||||||
INSERT INTO data_connection_definitions (name, protocol, configuration, updated_at)
|
INSERT INTO data_connection_definitions (name, protocol, configuration, backup_configuration, failover_retry_count, updated_at)
|
||||||
VALUES (@name, @protocol, @config, @updatedAt)
|
VALUES (@name, @protocol, @config, @backupConfig, @failoverRetryCount, @updatedAt)
|
||||||
ON CONFLICT(name) DO UPDATE SET
|
ON CONFLICT(name) DO UPDATE SET
|
||||||
protocol = excluded.protocol,
|
protocol = excluded.protocol,
|
||||||
configuration = excluded.configuration,
|
configuration = excluded.configuration,
|
||||||
|
backup_configuration = excluded.backup_configuration,
|
||||||
|
failover_retry_count = excluded.failover_retry_count,
|
||||||
updated_at = excluded.updated_at";
|
updated_at = excluded.updated_at";
|
||||||
|
|
||||||
command.Parameters.AddWithValue("@name", name);
|
command.Parameters.AddWithValue("@name", name);
|
||||||
command.Parameters.AddWithValue("@protocol", protocol);
|
command.Parameters.AddWithValue("@protocol", protocol);
|
||||||
command.Parameters.AddWithValue("@config", (object?)configJson ?? DBNull.Value);
|
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"));
|
command.Parameters.AddWithValue("@updatedAt", DateTimeOffset.UtcNow.ToString("O"));
|
||||||
|
|
||||||
await command.ExecuteNonQueryAsync();
|
await command.ExecuteNonQueryAsync();
|
||||||
@@ -512,7 +519,7 @@ public class SiteStorageService
|
|||||||
await connection.OpenAsync();
|
await connection.OpenAsync();
|
||||||
|
|
||||||
await using var command = connection.CreateCommand();
|
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>();
|
var results = new List<StoredDataConnectionDefinition>();
|
||||||
await using var reader = await command.ExecuteReaderAsync();
|
await using var reader = await command.ExecuteReaderAsync();
|
||||||
@@ -522,7 +529,9 @@ public class SiteStorageService
|
|||||||
{
|
{
|
||||||
Name = reader.GetString(0),
|
Name = reader.GetString(0),
|
||||||
Protocol = reader.GetString(1),
|
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 Name { get; init; } = string.Empty;
|
||||||
public string Protocol { get; init; } = string.Empty;
|
public string Protocol { get; init; } = string.Empty;
|
||||||
public string? ConfigurationJson { get; init; }
|
public string? ConfigurationJson { get; init; }
|
||||||
|
public string? BackupConfigurationJson { get; init; }
|
||||||
|
public int FailoverRetryCount { get; init; } = 3;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,7 +96,8 @@ public class SiteService
|
|||||||
// --- Data Connection CRUD ---
|
// --- Data Connection CRUD ---
|
||||||
|
|
||||||
public async Task<Result<DataConnection>> CreateDataConnectionAsync(
|
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)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
@@ -104,7 +105,12 @@ public class SiteService
|
|||||||
if (string.IsNullOrWhiteSpace(protocol))
|
if (string.IsNullOrWhiteSpace(protocol))
|
||||||
return Result<DataConnection>.Failure("Protocol is required.");
|
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.AddDataConnectionAsync(connection, cancellationToken);
|
||||||
await _repository.SaveChangesAsync(cancellationToken);
|
await _repository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
@@ -115,7 +121,8 @@ public class SiteService
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<DataConnection>> UpdateDataConnectionAsync(
|
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)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var connection = await _repository.GetDataConnectionByIdAsync(connectionId, cancellationToken);
|
var connection = await _repository.GetDataConnectionByIdAsync(connectionId, cancellationToken);
|
||||||
@@ -124,7 +131,9 @@ public class SiteService
|
|||||||
|
|
||||||
connection.Name = name;
|
connection.Name = name;
|
||||||
connection.Protocol = protocol;
|
connection.Protocol = protocol;
|
||||||
connection.PrimaryConfiguration = configuration;
|
connection.PrimaryConfiguration = primaryConfiguration;
|
||||||
|
connection.BackupConfiguration = backupConfiguration;
|
||||||
|
connection.FailoverRetryCount = failoverRetryCount;
|
||||||
await _repository.UpdateDataConnectionAsync(connection, cancellationToken);
|
await _repository.UpdateDataConnectionAsync(connection, cancellationToken);
|
||||||
await _repository.SaveChangesAsync(cancellationToken);
|
await _repository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ public class SiteServiceTests
|
|||||||
_repoMock.Setup(r => r.SaveChangesAsync(It.IsAny<CancellationToken>()))
|
_repoMock.Setup(r => r.SaveChangesAsync(It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync(1);
|
.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.True(result.IsSuccess);
|
||||||
Assert.Equal("OPC-Server1", result.Value.Name);
|
Assert.Equal("OPC-Server1", result.Value.Name);
|
||||||
|
|||||||
Reference in New Issue
Block a user